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
Leave a Reply