Posts tagged ‘xmljavatypeadapter’

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 🙂