Posts tagged ‘xmladapter’

Jakiś czas temu pisałem o dodawaniu adapterów do atrybutów poprzez namespace jaxb, a konkretnie korzystając z elementu jaxb:javaType. Zmiana ta umożliwia eleganckie korzystanie z Map’y po stronie javy (nawet silnie typizowanej z moim hakiem) a także zapis do xml’a string’ów pałkowo-średnikowych. Jednak jak wiadomo taki string to dość toporna struktura danych i jego utrzymanie dość szybko napotkało swoje naturalne ograniczenia:

  • po pierwsze rozmiar – po dodaniu paru elementów string pałkowo-średnikowy przestaje być czytelny
  • po drugie kontrola wersji – zmiana choćby jednego znaku w stringu pałkowo-średnikowym widoczna jest podczas sprawdzania i checkinowania zmian jako zmiana w jednej długaśnej linii, więc jakakolwiek kontrola tego co zostało zmienione staje się bardzo trudna, no i historia w kontorli wersji na mało się przydaje, no chyba, że ktoś ma wbudowany parser. Ja akurat takiego nie mam, więc każda zmiana to droga przez mękę.

Jak widać problem nabrał sporej rangi, no i pojawiło się pytanie czy nie dałoby się takiego rozwiązania zastąpić czymś bardziej czytelnym, czymś co oddawałoby strukture bez potrzeby łapania dziwnej zwiechy przy kolejnym zetknieciu z tym rozwiązaniem (zwiecha częściowo poświęcona na zadume i ciepłe słowa dla autora a częściowo  na sprawdzenie czy czasem się nie cierpi na jakąś chorobę oczu).

Pierwsze co przychodzi do głowy to zaaplikowanie kolejnego adaptera tym razem zamiast do atrybutu to do samego elementu. Pierwsze podejście to próba wdrożenia rozwiązania przedstawionego przez  Koshuke Kawaguchi już zresztą parę lat temu , wyglada na bułkę z masełkiem… Niestety jakoś nie udało mi się go skłonić do współpracy. Później wymyśliłem, żeby zamiast przekładania Listy zamienić ją na zwykłą tablicę, bo może dlatego adapter nie chwyta – niestety, tym razem już brak wyjątków, ale lista jest pusta. Podejście trzecie – wstawienie interfejsu kończy się wyjątkiem, bo JAXB nie obsługuje interfejsów. Aż tu jak już zrezygnowany ot tak zacząłem sobie szukać czy ktoś cokolwiek wymyślił w tej kwestii, natrafiłem na zbiór (działąjących!) tutoriali dla jee5 a w nich cały dział poświęcony JAXB. Niestety interesujący mnie przykład zwiera tylko klasy z adnotacjami, bez schemy. Postanowiłem troche poszukać, bo może udałoby się znaleźć jakis inny, który aplikuje adaptera do elementu, wtedy po połączeniu obu możnaby wygenerować całość ze schemy. No i faktycznie, znalazłem to czego potrzebuję.

Ostateczny wynik to:

  • dodanie elementu jaxb:Type jako podelementu (…) bez nadrzędnego jaxb:property
  • dodanie klasy zawijającej Map’ę czy też inna kolekcję (tak, żeby JAXB mogło operować na konkretnym typie)
  • dorzucenie adaptera
  • odpalenie JXC

A schema wyglada tak:


<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xs:schema version="1.0" xmlns:xs="http://www.w3.org/2001/XMLSchema"
xmlns:jxb="http://java.sun.com/xml/ns/jaxb" xmlns:xjc="http://java.sun.com/xml/ns/jaxb/xjc"
jxb:extensionBindingPrefixes="xjc" jxb:version="2.0"
targetNamespace="http://bedkowski.pl/elementAdapter" xmlns="http://bedkowski.pl/elementAdapter">

<xs:annotation>
<xs:appinfo>
<jxb:schemaBindings>
<jxb:package name="bla" />
</jxb:schemaBindings>
</xs:appinfo>
</xs:annotation>

<xs:element name="kitchenWorldBasket" type="KitchenWorldBasketType" />

<xs:element name="purchaseList" type="PurchaseListType">

</xs:element>

<xs:complexType name="KitchenWorldBasketType">
<xs:sequence>
<xs:element name="basket" minOccurs="0">
<xs:simpleType>
<xs:annotation>
<xs:appinfo>
<xjc:javaType name="java.util.Map" adapter="bla.AdapterPurchaseListToHashMap" />
</xs:appinfo>
</xs:annotation>

<xs:restriction base="xs:positiveInteger">
<xs:maxExclusive value="100" />
</xs:restriction>
</xs:simpleType>

</xs:element>
</xs:sequence>
</xs:complexType>

<xs:complexType name="PurchaseListType">
<xs:sequence>
<xs:element name="entry" type="partEntry" nillable="true"
minOccurs="0" maxOccurs="unbounded" />
</xs:sequence>
</xs:complexType>

<xs:complexType name="partEntry">
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="key" type="xs:int" use="required" />
</xs:extension>
</xs:simpleContent>
</xs:complexType>

</xs:schema>

Batalii z JAXB 2.1 ciąg dalszy 🙂

Po tym jak udało mi się zmusić XJC do wygenerowania schemy, dodania do wygenerowanego kodu @XmlJavaTypeAdapter i zamiany „w locie” atrybutu na kolekcje (opisane tutaj) pojawił sie mały problem. Po próbie dodania do <xjb:javaType /> silnie typizowanej kolekcji w następujący sposób:

<xjc:javaType name="java.util.Map&lt;String,String&gt;" adapter="com.nsn.tariffmig.parser.output.LliLibReferenceAdapter" />

<!-- tak tak pamietajcie, że to xml, więc > i < trzeba zapisywać jako encje -->


Pojawił się następujący problem w wygenerowanym kodzie:


import java.util.Map<String,String>;

Czyli nie całkiem to o co chodziło 🙁 Na szczęście źródła do JAXB są dostępne, więc postanowiłem sprawdzić ile zajmie znalezienie kodu, odpowiedzialnego za wypluwanie „import” cośtam. Okazało się to bajecznie proste, szybki grep:

grep -nirs 'import' *.java | less

Pojawiło się dość sporo wyników ale przecież import to słowo kluczowe w Javie. Z drugiej jednak strony nietrudno było odróżnić to czego szukałem, bo większość wpisów to prawdziwe importy, np:

com/sun/codemodel/JPackage.java:50:import java.util.Iterator;
com/sun/codemodel/JPackage.java:51:import java.util.List;
com/sun/codemodel/JPackage.java:52:import java.util.Map;

Natomiast ja szukałem tego:

com/sun/codemodel/JFormatter.java:110: importedClasses = new HashSet<JClass>();
com/sun/codemodel/JFormatter.java:266: * decide what types to import and what not to.
com/sun/codemodel/JFormatter.java:273: if(importedClasses.contains(type)) {

Teraz wystarczy tylko dodać „wymazywanie typu :D”, czyli zamienić linię 438 z:

p(„import”).p(clazz.fullName()).p(‚;’).nl();

na:

p(„import”).p(clazz.fullName().replaceAll(„<.*?>$”, „”)).p(‚;’).nl();

I już – kompilujemy, wkładamy to do jara (nie zapomnijcie o licencjach) i już możemy odpalać taska XJC z adapterami do silnie typizowanych kolekcji.

Prawda, że proste…

Wyobraźmy sobie, że mamy aplikację, która przetwarza dane na podstawie skomplikowanej konfiguracji w plikach xml, których struktura jest opisana przez odpowiadające im DTD. Tak tak wiem, pierwszy pomysł, to wywalić DTD i zastąpić je na schema. Brzmi nieźle, ale jest parę problemów, które trzeba rozważyć:

  • część plików DTD pochodzi z zewnątrz, więc ich zmiana nie wchodzi w grę
  • struktura, którą opisują jest dość skomplikowana i przepisanie jej ręcznie może zająć nawet pare dni, a to nie jest to co tygryski lubią najbardziej

Ale nie jest beznadziejnie, gdyż sun jakiś czas temu dostarczył dość miłe narędzie ukrywające manipulacje drzewem XML’owym za obiektami JavaBean – JAXB. Naszą wycieczkę rozpoczniemy od miłego nardzędzia – XJC, umożwliającego wygenerowanie klas z istniejącego DTD/Schema.

No więc do dzieła, szybki task ant’owy i można jechać z koksem. Jednak pozostaje mały problem – niektóre z atrybutów, mają ograniczoną ilość elementów, ale niestety  z uwagi na to, że jedyny typ jaki im można przypisać to string silną typizację trafia szlag :/

Jest jednak i na to rozwiązanie, coprawda nieco zakręcone i trochę naokoło, ale wynik jest zadowalający. W skład JAXB, poza narędziem XJC jest także SchemaGen, dostępne również jako SchemaGenTask dla ant’a, które to umożliwia wygenerowanie schem’y na podstawie gotowych klas i adnotacji.

Czyli najpierw z DTD robimy klasy przy pomocy XJC, a później zamieniamy na scheme’a przy użyciu SchemaGen (ostrzegałem, że to troche naokoło 🙂 ).

No i dzięki temu dla częśći klas mamy schem’e, co daje możliwości rozszerzenia jej poprzez namespace jaxb, jednak aby nie było zbyt prosto informacji jak na lekarstwo :/ Troche trzeba było się naszukać, ale znalazłem w końcu stronę z opisem tegoż namespace’a, wraz z przykładami i ku mojemu zdumieniu okazało się, że rozwiązania problemu dodania silnej typizacji do atrybutu są conajmniej 2:

  • można dodać enuma, z poziomu schem’y – trzeba wypisać elementy które ma zawierać a XJC zrobi resztę, wygeneruje klasę i zapewni silną kontrolę typów. Jedyny wymóg jest taki, żeby nazwy poszególnych elementów były identyczne z dozwolonymi elementami atrybutu (przykład ze strony powyżej)
    <xs:simpleType name="ApptType">
            <xs:annotation>
                <xs:appinfo>
                    <!-- Map the elements of this simple type to a Java typesafe enum class. -->
                    <jxb:typesafeEnumClass/>
                </xs:appinfo>
            </xs:annotation>
            <!-- Use XML Schema elements restriction and enumeration to define the supported appointment types. -->
            <xs:restriction base="xs:string">
                <xs:enumeration value="Yearly Checkup"/>
                <xs:enumeration value="Well Mom Exam"/>
                <xs:enumeration value="Teeth Cleaning"/>
                <xs:enumeration value="Vaccination"/>
                <xs:enumeration value="Senior Pet Checkup"/>
            </xs:restriction>
        </xs:simpleType>
    
  • można dodać mapowanie typu na string’a (lub inny prymityw schem’a) realizowane poprzez klasę rozszerzającą XmlAdapter, dzięki czemu w kodzie wygenerowanym ze schem’y zostanie wstawiona adnotacja XmlJavaTypeAdapter i podczas marshalling/unmarshallingu odbędzie się wywołanie naszego adaptera (przykład też ze strony)
    <xs:element name="printOrder" type="PrintOrderType"/>
        <xs:complexType name="PrintOrderType">
            <xs:sequence>
                <xs:element name="notifications" type="notificationsType" minOccurs="0"/>
            </xs:sequence>
            <xs:attribute name="id" type="xs:long" use="required">
                <xs:annotation>
                    <xs:appinfo>
                        <!-- Use an XmlAdapter-based adapter to create WePrintStuff's PrintOrderKey class. -->
                        <!-- Specify the name orderKey for this property. -->
                        <jxb:property name="orderKey">
                            <jxb:baseType>
                                <!-- Specify weprintstuff.print.PrintOrderKey as the type of the orderKey property. -->
                                <!-- Specify the adapter class that will map the schema type to the Java type. -->
                                <xjc:javaType name="weprintstuff.print.PrintOrderKey"
                                  adapter="weprintstuff.print.IdAdapter"/>
                            </jxb:baseType>
                        </jxb:property>
                    </xs:appinfo>
                </xs:annotation>
            </xs:attribute>
        </xs:complexType>
    

O ile dla nowych rozwiązań, chyba najszybszym podejściem będzie zastosowanie „automatycznej” zamiany typu na enum’a poprzez wypisanie i listy dostępnych elementów i pozwolanie XJC na zrobienie reszty, zamiast samemu definiować klase, która będzie odpowiedzialna za konwersje, o tyle ja szukałem akurat tego drugiego rozwiązania. Z uwagi na to, że wcześniej podododawałem enum’y w kodzie, razem z odpowiednimi klasami/metodami służącymi do ich zamiany. Wystarczyło więc przenieść kod do odpowiedniego Adaptor’a i już…

…i już już się z gąską witał…

Jednak okazało się, że to wcale nie koniec, bo po odpaleniu mojego generatora ze schemy dostałem błąd, że nie mogę używać xjc:javaType wewnątrz deklaracji i tutaj byłem naprawde zaskoczony, bo próba zastąpienia przez axb:javaType jak suegorwał wyjątek, kończyła się jeszcze gorzej. Jak się okazało, błąd był w samym JAXB i przejście na wersję 2.1.11 poprawiło sytuację, ale okazało się, że task ant’owy zamiast atrybutu target posiada teraz destdir, więc trzeba było jeszcze to zapuścić i klasy się w końcu wygenerowały.

Możliwości dodawania adapterów są nieograniczone, teraz tylko musze dojść jak zrobić coś takiego dla DTD, które pochodzą z zewnątrz. Podejrzewam, że da się to zrobić poprzez dodatkowy plik, który można dołączyć do mapowań (external binding customization files) ale to już sobie zostawię na następny wpis 🙂