Как стать автором
Поиск
Написать публикацию
Обновить

«Выпрямляем» 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 аннотацией:
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.
Теги:
Хабы:
Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.