«Выпрямляем» JAXB с помощью Groovy AST трансформаций
Ожидает приглашения
В процессе создания рабочего прототипа небольшой event management системы на базе EventBrite-а выяснилось, что готового Java клиента, к нашему удивлению, нет. Решили использовать связку Groovy + Jersey Client + JAXB + XML, чтобы заодно попробовать JAX-RS 2.0 Client API. В качестве формата запрашиваемых данных взяли XML, чтобы избежать на начальном этапе потенциальных нюансов с парсингом JSON-а.
Начав создавать POGO для сущности Attendee, обратили внимание, что сама сущность содержит 3 адреса с практически одинаковым набором полей: «home_*», «work_*» и «ship_*». Возникла идея вынести адрес в отдельный класс, и использовать что-то идентичное Composition in GORM, только для JAXB.
Большинство найденных решених сводились к использованию расширенных возможностей EclipseLink JAXB (MOXy). Но в одном из ответов встретился интересный подход в виде делегации свойств встраиваемого класса через создание приватных синтетических свойств в классе «собственнике» — getter/setter методов с соответствующей JAXB аннотацией:
Тем самым мы получали XML желаемой структуры и решение отлично работало в обе стороны:
В чистом виде данный подход предназначался для точечных решений и на универсальность явно не тянул. В случае изменения структуры встраиваемого класса необходимо менять и класс собственника. Но в случае с Groovy решение лежало на поверхности — генерация необходимого кода с помощью механизма AST (Abastract Syntax Tree) трансформаций.
В связи с отсутствием опыта написания AST транформаций за основу взяли реализацию DelegateASTTransformation для Delegate аннотации.
Аннотация:
Объявляем, что наша
В реализации метода
Создаем тестовую модель данных:
Пишем тест:
XML на выходе (как и на входе):
Исходный код доступен на GitHub.
Начав создавать POGO для сущности Attendee, обратили внимание, что сама сущность содержит 3 адреса с практически одинаковым набором полей: «home_*», «work_*» и «ship_*». Возникла идея вынести адрес в отдельный класс, и использовать что-то идентичное Composition in GORM, только для JAXB.
Большинство найденных решених сводились к использованию расширенных возможностей EclipseLink JAXB (MOXy). Но в одном из ответов встретился интересный подход в виде делегации свойств встраиваемого класса через создание приватных синтетических свойств в классе «собственнике» — getter/setter методов с соответствующей JAXB аннотацией:
class B {
String propertyOfB;
....
}
@XmlRootElement
class A {
@XmlElement
String propertyOfA;
....
@XmlTransient
B b;
@XmlElement(name = "b_propertyOfB")
private String getB_propertyOfB() {
return b != null ? b.getPropertyOfB() : null;
}
private void setB_propertyOfB(String propertyOfB) {
if (b == null)
b = new B();
b.setPropertyOfB(propertyOfB);
}
}
Тем самым мы получали XML желаемой структуры и решение отлично работало в обе стороны:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<a>
<propertyOfA>some prop of A</propertyOfA>
<b_propertyOfB>some prop of B</b_propertyOfB>
</a>
В чистом виде данный подход предназначался для точечных решений и на универсальность явно не тянул. В случае изменения структуры встраиваемого класса необходимо менять и класс собственника. Но в случае с Groovy решение лежало на поверхности — генерация необходимого кода с помощью механизма AST (Abastract Syntax Tree) трансформаций.
В связи с отсутствием опыта написания AST транформаций за основу взяли реализацию DelegateASTTransformation для Delegate аннотации.
Аннотация:
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.FIELD})
@GroovyASTTransformationClass({"com.godeltech.groovy.ast.XmlFlatElementASTTransformation"})
public @interface XmlFlatElement {
String prefix() default "##default";
}
Объявляем, что наша
XmlFlatElementASTTransformation
трансформация (полная версия кода на github) работает на фазе компиляции Canonicalization
:@GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION)
public class XmlFlatElementASTTransformation extends AbstractASTTransformation {}
В реализации метода
visit
после проверок проходим по свойствам поля и генерим getter/setter-ы, не забывая в конце добавить @XmlTransient
аннотацию самому полю: public void visit(ASTNode[] nodes, SourceUnit source) {
if (nodes.length != 2 || !(nodes[0] instanceof AnnotationNode) || !(nodes[1] instanceof AnnotatedNode)) {
throw new GroovyBugError("Internal error: expecting [AnnotationNode, AnnotatedNode] but got: "
+ Arrays.asList(nodes));
}
AnnotatedNode parent = (AnnotatedNode) nodes[1];
AnnotationNode node = (AnnotationNode) nodes[0];
if (parent instanceof FieldNode) {
...
for (PropertyNode prop : type.getProperties()) {
if (prop.isStatic() || !prop.isPublic())
continue;
addGetterSetter(fieldNode, owner, prop, prefix);
}
fieldNode.addAnnotation(new AnnotationNode(new ClassNode(XmlTransient.class)));
}
}
private void addGetterSetter(FieldNode fieldNode, ClassNode owner, PropertyNode prop, String prefix){
.....
}
Создаем тестовую модель данных:
@EqualsAndHashCode
class Address {
String address
String city
String country_code
}
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
@EqualsAndHashCode
class Attendee {
long id
String email
@XmlFlatElement(prefix = "home")
Address homeAddress
@XmlFlatElement
Address workAddress
}
Пишем тест:
def attendee = new Attendee(id: 1, email: "aaa@bbb.com",
homeAddress: new Address(country_code:"UK", city: "Manchester", address: "home street, 11"),
workAddress: new Address(country_code:"UK", city: "Northwich", address: "work street, 22"))
def out = new StringWriter()
JAXB.marshal(attendee, out)
println out
assert attendee == JAXB.unmarshal(new StringReader(out.toString()),Attendee.class)
XML на выходе (как и на входе):
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<attendee>
<id>1</id>
<email>aaa@bbb.com</email>
<home_address>home street, 11</home_address>
<home_city>Manchester</home_city>
<home_country_code>UK</home_country_code>
<workAddress_address>work street, 22</workAddress_address>
<workAddress_city>Northwich</workAddress_city>
<workAddress_country_code>UK</workAddress_country_code>
</attendee>
Исходный код доступен на GitHub.