czwartek, 10 stycznia 2013

JAXB - unmarshalling nested element

To fully benefit from this post I suggest visiting following tutorial on XSD and namespaces, and this one describing what is a globally declared element in XSD and why some of them get @XmlRootElement annotation and others have ObjectFactory with method annotated with @XmlElementDecl. The general rule is that if we have globally declared element (not type, element) which contains:
  1. Inline type declaration (anonymous type, as in case of kitchen element from rooms.xsd), it will be annotated with @XmlRootElement
  2. Reference to named type declaration (like houseObject element in houses.xsd), it will have corresponding ObjectFactory with createXXX method annotaded with @XmlElementDecl
As an intro to this post, let's create some xsd files on which we will work: house.xsd
<xs:schema xmlns:house="http://houses.service.jaxb.mpasinski/"
           xmlns:xs="http://www.w3.org/2001/XMLSchema"
           xmlns:rooms="http://rooms.houses.service.jaxb.mpasinski/"
           version="1.0"
           targetNamespace="http://houses.service.jaxb.mpasinski/">
    <xs:import namespace="http://rooms.houses.service.jaxb.mpasinski/" schemaLocation="rooms.xsd"/>

    <!-- globally defined element with named type -->
    <!-- it will result in XmlElementDecl annotation in ObjectFactory -->
    <xs:element name="houseObject" type="house:house"/>

    <!--globally defined elements with anonymous type (inline definition)-->
    <!--it will result in XmlElementRoot annotation to be added to generated class-->
    <xs:element name="houseRequest">
        <xs:complexType>
            <xs:sequence>
                <xs:element ref="house:houseObject"/>
                <xs:element name="requestDateTime" type="xs:dateTime"/>
            </xs:sequence>
        </xs:complexType>
    </xs:element>

    <xs:element name="houseLockedRequest">
        <xs:complexType>
            <xs:sequence>
                <xs:element ref="house:houseObject"/>
            </xs:sequence>
        </xs:complexType>
    </xs:element>

    <xs:element name="isHouseLockedResponse" type="xs:boolean"></xs:element>

    <!-- Type definition -->
    <xs:complexType name="house">
        <xs:sequence>
            <xs:element name="isLocked" type="xs:boolean" minOccurs="0"/>
            <xs:element ref="rooms:kitchen" minOccurs="0"/>
            <xs:element name="livingRoom" type="rooms:livingRoom" minOccurs="0"/>
        </xs:sequence>
    </xs:complexType>
</xs:schema>
rooms.xsd
<xs:schema
        xmlns:furniture="http://furniture.service.jaxb.mpasinski/"
        xmlns:xs="http://www.w3.org/2001/XMLSchema"
        version="1.0"
        targetNamespace="http://rooms.houses.service.jaxb.mpasinski/">
    <xs:import namespace="http://furniture.service.jaxb.mpasinski/" schemaLocation="furniture.xsd"/>

    <!--below is the globally defined element with anonymous type-->
    <!--jaxb will add XmlRoot annotation to the class-->

    <xs:element name="kitchen">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="cupboard" type="furniture:cupboard" minOccurs="0"/>
                <xs:element name="fridge" type="furniture:fridge" minOccurs="0"/>
                <xs:element name="isLocked" type="xs:boolean" minOccurs="0"/>
            </xs:sequence>
        </xs:complexType>
    </xs:element>

    <!--type declaration-->
    <xs:complexType name="livingRoom">
        <xs:sequence>
            <xs:element name="isLocked" type="xs:boolean" minOccurs="0"/>
        </xs:sequence>
    </xs:complexType>
</xs:schema>
furniture.xsd
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"
           version="1.0"
           targetNamespace="http://furniture.service.jaxb.mpasinski/"
           elementFormDefault="qualified">

    <!--contains only type declarations, no globally defined elements-->
    <xs:complexType name="cupboard">
        <xs:sequence>
            <xs:element name="capacity" type="xs:int" minOccurs="0"/>
        </xs:sequence>
    </xs:complexType>
    <xs:complexType name="fridge">
        <xs:sequence>
            <xs:element name="boughtOn" type="xs:dateTime" minOccurs="0"/>
            <xs:element name="innerTemperature" type="xs:int" minOccurs="0"/>
        </xs:sequence>
    </xs:complexType>
</xs:schema>
Now, let's say we received an xml request document with following format:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<houses:houseRequest xmlns:furniture="http://furniture.service.jaxb.mpasinski/" xmlns:houses="http://houses.service.jaxb.mpasinski/" xmlns:rooms="http://rooms.houses.service.jaxb.mpasinski/">
    <houses:houseObject>
        <isLocked>true</isLocked>
        <rooms:kitchen>
            <cupboard>
                <furniture:capacity>8</furniture:capacity>
            </cupboard>
            <fridge>
                <furniture:boughtOn>2013-01-10T20:45:45.033+01:00</furniture:boughtOn>
                <furniture:innerTemperature>-4</furniture:innerTemperature>
            </fridge>
            <isLocked>true</isLocked>
        </rooms:kitchen>
        <livingRoom>
            <isLocked>true</isLocked>
        </livingRoom>
    </houses:houseObject>
    <requestDateTime>2013-01-10T20:45:45.037+01:00</requestDateTime>
</houses:houseRequest>

We do not care for the for the whole request, but only for kitchen object. Our first attempt to get to the element might be like that:

        String houseRequest = TestUtils.readClasspathFile("houseRequest.xml");

        JAXBContext jaxbContext = JaxbUtils.getJAXBContext(Kitchen.class);
        JAXBElement<Kitchen> result = jaxbContext.createUnmarshaller().unmarshal(new StreamSource(new ByteArrayInputStream(houseRequest.getBytes())), Kitchen.class);
        Kitchen unmarshalledKitchen =  result.getValue();
But after inspecting the retreived kitchen object we find out that both cupboard and fridge are null:
Kitchen@5749b290[cupboard=<null>,fridge=<null>,isLocked=true]

The reason for that is simple. XML does not convey information about type, so JAXB does not know it should start searching for the kitchen element starting from kitchen tag. For the same reason, isLocked in our unmarshalled kitchen is set properly - JAXB found element isLocked (as it expected for kitchen object) and it does not care it that it belongs to houseObject (so different Java type). The reason why our unmarshalling faild becomes even more obvious when we look at result object (of JAXBElement type)

javax.xml.bind.JAXBElement@276a38b5[name={http://houses.service.jaxb.mpasinski/}houseRequest,
declaredType=class mpasinski.jaxb.service.houses.rooms.Kitchen,
scope=class javax.xml.bind.JAXBElement$GlobalScope,
value=mpasinski.jaxb.service.houses.rooms.Kitchen@6765f738,
nil=false]

Unmarshalling nested element with StAX and XMLReader

First we need to tell JAXB where to start unmarshalling our object:

String houseRequest = TestUtils.readClasspathFile("houseRequest.xml");
String nodeName = "kitchen";
Kitchen unmarshalledKitchen = null;

//create XMLStreamReader (StAX)
XMLInputFactory xif = XMLInputFactory.newFactory();
XMLStreamReader xmlReader = xif.createXMLStreamReader(new StringReader(houseRequest));

int event = 0;
//here we advance to next tag untill we find node called "kitchen"
for (event = xmlReader.next(); event != XMLStreamReader.END_DOCUMENT; event = xmlReader.next()) {
    if (event == XMLStreamReader.START_ELEMENT) {
        if (xmlReader.getLocalName() == nodeName) {
            break;
        }
    }
}

Now all we need to do is just unmarshalling

        if (event != XMLStreamReader.END_DOCUMENT) {
            JAXBContext jaxbContext = JaxbUtils.getJAXBContext(Kitchen.class);
            JAXBElement<Kitchen> result = jaxbContext.createUnmarshaller().unmarshal(xmlReader, Kitchen.class);
            unmarshalledKitchen = result.getValue();
        }

If we analyze our unmarshalled object now, we get following form:

mpasinski.jaxb.service.houses.rooms.Kitchen@4b00ebec
 [cupboard=mpasinski.jaxb.service.furniture.Cupboard@2980f96c,
  fridge=mpasinski.jaxb.service.furniture.Fridge@527736bd,
  isLocked=true]
//cupboard
mpasinski.jaxb.service.furniture.Cupboard@2980f96c[capacity=8]
//fridge
mpasinski.jaxb.service.furniture.Fridge@527736bd
 [boughtOn=java.util.GregorianCalendar[time=?,areFieldsSet=false,areAllFieldsSet=true,lenient=true,zone=sun.util.calendar.ZoneInfo[id="GMT+01:00",offset=3600000,dstSavings=0,useDaylight=false,transitions=0,lastRule=null],firstDayOfWeek=2,minimalDaysInFirstWeek=4,ERA=1,YEAR=2013,MONTH=0,WEEK_OF_YEAR=1,WEEK_OF_MONTH=1,DAY_OF_MONTH=10,DAY_OF_YEAR=1,DAY_OF_WEEK=5,DAY_OF_WEEK_IN_MONTH=1,AM_PM=0,HOUR=0,HOUR_OF_DAY=20,MINUTE=45,SECOND=45,MILLISECOND=33,ZONE_OFFSET=3600000,DST_OFFSET=0],
  innerTemperature=-4]

On my github repo you can find JaxbUtils class which has method doUnmsarshallNestedElement being refactored version of the code above. You can find there also the complete code used in this post.

Of course the definite flaw of this method is that it will try to unmarshall first element with specified name. A better way to perform unmarshalling nested element may be to use EclipseLink MOXy jaxb implementation and the @XmlPath extension. However, there are times where you cannot change the jaxb implementation or you cannot alter generated classes (therefore you cannot add @XmlPath annotation)

Brak komentarzy:

Prześlij komentarz