Friday, January 11, 2013

Spring WS and JAXB

Marshalling XML with Spring WS and JAXB

The ‘object-relational impedance mismatch’ is a well documented set of conceptual and technical difficulties that are often encountered when a relational database management system is being used by a program written in an object-oriented programming language. A similar impedance mismatch exists when XML is used by a program written in an object-oriented programming language.
Many popular ‘Object-Relational Mapping’ (ORM) frameworks exist that address the object-relational impedance mismatch and no doubt helped to inspire the evolution of ‘Object-XML Mapping’ (OXM) frameworks to address the object-xml impedance mismatch.
For the Java community, there are a number of OXM frameworks from which to choose (Castor, XStream, JiBX, JAXB) with each having particular strengths and weaknesses. The standard OXM framework for Java is JAXB.
In the following I work through a simple example that demonstrates object to XML marshalling (and demarshalling) using Spring, Spring WS and JAXB (and later JAXB Introductions!)
I have provided Maven projects for download so you can follow along:
The model object that I have used for this example is a simple object representing a ‘Person’. The class contains only simple attributes.
Listing 1: Person.java
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package net.thoughtforge.model;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Calendar;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
@XmlRootElement(
        name="Person",
        namespace="http://thoughtforge.net/model")
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType
public class Person implements Serializable {
    private static final long serialVersionUID = 8465162879793776395L;
    @XmlElement(namespace="http://thoughtforge.net/model")
    private Calendar dateOfBirth;
    @XmlElement(namespace="http://thoughtforge.net/model")
    private String firstName;
    @XmlElement(namespace="http://thoughtforge.net/model")
    private BigDecimal height;
    @XmlElement(namespace="http://thoughtforge.net/model")
    private String lastName;
    @XmlElement(namespace="http://thoughtforge.net/model")
    private BigDecimal weight;
    public Calendar getDateOfBirth() {
        return dateOfBirth;
    }
    public void setDateOfBirth(Calendar dateOfBirth) {
        this.dateOfBirth = dateOfBirth;
    }
    public String getFirstName() {
        return firstName;
    }
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
    public BigDecimal getHeight() {
        return height;
    }
    public void setHeight(BigDecimal height) {
        this.height = height;
    }
    public String getLastName() {
        return lastName;
    }
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
    public BigDecimal getWeight() {
        return weight;
    }
    public void setWeight(BigDecimal weight) {
        this.weight = weight;
    }
}
As you can see from the listing, the class is annotated with JAXB annotations that are fairly self explanatory. I will leave it to you to look up the precise definition and consequence of these annotations (which can be found on the JAXB Reference Implementation website).
The XML Schema (XSD)
JAXB does not require an XML schema (a default schema can be generated), but for this example (and always on commercial projects) I have explicitly specified a schema in order to include type restrictions. It is important to remember that the XSD forms the contract between XML producers and consumers (marshaller and unmarshaller) and should be as detailed as possible.
Listing 2: person.xsd
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
    targetNamespace="http://thoughtforge.net/model"
    xmlns:tns="http://thoughtforge.net/model"
    attributeFormDefault="unqualified"
    elementFormDefault="qualified"
    version="1.0">
    <xsd:element name="Person" type="tns:Person"/>
    <xsd:complexType name="Person">
        <xsd:sequence>
            <xsd:element name="dateOfBirth" type="xsd:dateTime"/>
            <xsd:element name="firstName">
                <xsd:simpleType>
                    <xsd:restriction base="xsd:string">
                        <xsd:maxLength value="50"/>
                    </xsd:restriction>
                </xsd:simpleType>
            </xsd:element>
            <xsd:element name="height">
                <xsd:simpleType>
                    <xsd:restriction base="xsd:decimal">
                        <xsd:fractionDigits value="2"/>
                    </xsd:restriction>
                </xsd:simpleType>
            </xsd:element>
            <xsd:element name="lastName">
                <xsd:simpleType>
                    <xsd:restriction base="xsd:string">
                        <xsd:maxLength value="50"/>
                    </xsd:restriction>
                </xsd:simpleType>
            </xsd:element>
            <xsd:element name="weight">
                <xsd:simpleType>
                    <xsd:restriction base="xsd:decimal">
                        <xsd:fractionDigits value="2"/>
                    </xsd:restriction>
                </xsd:simpleType>
            </xsd:element>
        </xsd:sequence>
    </xsd:complexType>
</xsd:schema>

The Marshaller Abstraction

Rather than use the Spring Marshaller/Unmarshaller interface directly, I often use an abstraction for the Marshaller. The abstraction I used here is very simple and of course would not be practical for use with large XML files as it does not support streaming etc.
Listing 3: Marshaller.java
01
02
03
04
05
06
07
08
package net.thoughtforge.marshaller;
public interface Marshaller {
    String marshal(Object object);
    Object unmarshal(String string);
}
The implementation of the marshaller delegates to a Spring JAXB marshaller.
Listing 4: Jaxb2Marshaller.java
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package net.thoughtforge.marshaller;
import java.io.StringWriter;
import javax.xml.transform.stream.StreamResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import org.springframework.xml.transform.StringSource;
@Component(value="marshaller")
public class Jaxb2Marshaller implements Marshaller {
    @Autowired
    @Qualifier(value="jaxb2Marshaller")
    private org.springframework.oxm.jaxb.Jaxb2Marshaller marshaller;
    public String marshal(Object object) {
        final StringWriter out = new StringWriter();
        marshaller.marshal(object, new StreamResult(out));
        return out.toString();
    }
    public Object unmarshal(String string) {
        return marshaller.unmarshal(new StringSource(string));
    }
}

The Configuration

Using Spring to define a JAXB marshaller is relatively straight forward as the listing shows. I have specified the classes to be bound and the XML schema. If the XML schema is specified, the marshaller will instruct the XML parser to validate XML against the schema.
Listing 5: applicationContext-marshaller.xml
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="UTF-8"?>
    <context:component-scan base-package="net.thoughtforge.marshaller"/>
    <bean id="jaxb2Marshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
        <property name="classesToBeBound">
            <list>
                <value>net.thoughtforge.model.Person</value>
            </list>
        </property>
        <property name="schema" value="classpath:schema/person.xsd"/>
    </bean>
</beans>

The Test

The following test is not exhaustive but demonstrates the marshalling and unmarshalling of a Person object. It also demonstrates XML schema validation occurring during the marshalling and unmarshalling process.
Listing 6: Jaxb2MarshallerTest.java
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
package net.thoughtforge.marshaller;
import java.math.BigDecimal;
import java.util.Calendar;
import net.thoughtforge.model.Person;
import org.apache.commons.lang.text.StrBuilder;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.oxm.MarshallingFailureException;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {
        "classpath:applicationContext/applicationContext-*.xml"})
public class Jaxb2MarshallerTest {
    private static final String MARSHALLED_PERSON =
        "1965-01-01T00:00:00ZJoe1.85Bloggs12.2";
    private static Calendar dateOfBirth;
    private static String firstName;
    private static BigDecimal height;
    private static String lastName;
    private static BigDecimal weight;
    @Autowired
    @Qualifier(value="marshaller")
    private Jaxb2Marshaller marshaller;
    @BeforeClass
    public static void beforeClass() {
        dateOfBirth = Calendar.getInstance();
        dateOfBirth.clear();
        dateOfBirth.set(Calendar.DATE, 1);
        dateOfBirth.set(Calendar.MONTH, Calendar.JANUARY);
        dateOfBirth.set(Calendar.YEAR, 1965);
        firstName = "Joe";
        height = new BigDecimal("1.85");
        lastName = "Bloggs";
        weight = new BigDecimal("12.2");
    }
    @Test
    public void marshallPerson() {
        Person person = new Person();
        person.setDateOfBirth(dateOfBirth);
        person.setFirstName(firstName);
        person.setHeight(height);
        person.setLastName(lastName);
        person.setWeight(weight);
        String xml = marshaller.marshal(person);
        Assert.assertNotNull(xml);
        Assert.assertEquals(MARSHALLED_PERSON, xml);
    }
    @Test
    public void marshallPersonInvalidFirstName() {
        Person person = new Person();
        person.setDateOfBirth(dateOfBirth);
        person.setFirstName(new StrBuilder(firstName).appendPadding(50, '0').toString());
        person.setHeight(height);
        person.setLastName(lastName);
        person.setWeight(weight);
        try {
            marshaller.marshal(person);
            Assert.fail("First name length restriction not applied.");
        } catch (MarshallingFailureException marshallingFailureException) {
            Throwable rootCause = marshallingFailureException.getRootCause();
            Assert.assertFalse(rootCause.getMessage().indexOf("is not facet-valid with respect to maxLength '50'") == -1);
        }
    }
    @Test
    public void marshallPersonInvalidHeight() {
        Person person = new Person();
        person.setDateOfBirth(dateOfBirth);
        person.setFirstName(firstName);
        person.setHeight(height.add(new BigDecimal("0.1111")));
        person.setLastName(lastName);
        person.setWeight(weight);
        try {
            marshaller.marshal(person);
            Assert.fail("Height precision restriction not applied.");
        } catch (MarshallingFailureException marshallingFailureException) {
            Throwable rootCause = marshallingFailureException.getRootCause();
            Assert.assertFalse(rootCause.getMessage().indexOf("the number of fraction digits has been limited to 2") == -1);
        }
    }
    @Test
    public void marshallPersonInvalidLastName() {
        Person person = new Person();
        person.setDateOfBirth(dateOfBirth);
        person.setFirstName(firstName);
        person.setHeight(height);
        person.setLastName(new StrBuilder(lastName).appendPadding(50, '0').toString());
        person.setWeight(weight);
        try {
            marshaller.marshal(person);
            Assert.fail("First name length restriction not applied.");
        } catch (MarshallingFailureException marshallingFailureException) {
            Throwable rootCause = marshallingFailureException.getRootCause();
            Assert.assertFalse(rootCause.getMessage().indexOf("is not facet-valid with respect to maxLength '50'") == -1);
        }
    }
    @Test
    public void marshallPersonInvalidWeight() {
        Person person = new Person();
        person.setDateOfBirth(dateOfBirth);
        person.setFirstName(firstName);
        person.setHeight(height);
        person.setLastName(lastName);
        person.setWeight(weight.add(new BigDecimal("0.1111")));
        try {
            marshaller.marshal(person);
            Assert.fail("Weight precision restriction not applied.");
        } catch (MarshallingFailureException marshallingFailureException) {
            Throwable rootCause = marshallingFailureException.getRootCause();
            Assert.assertFalse(rootCause.getMessage().indexOf("the number of fraction digits has been limited to 2") == -1);
        }
    }
    @Test
    public void unmarshallPerson() {
        Person person = (Person) marshaller.unmarshal(MARSHALLED_PERSON);
        Assert.assertNotNull(person);
        Assert.assertTrue(dateOfBirth.compareTo(person.getDateOfBirth()) == 0);
        Assert.assertEquals(firstName, person.getFirstName());
        Assert.assertEquals(height, person.getHeight());
        Assert.assertEquals(lastName, person.getLastName());
        Assert.assertEquals(weight, person.getWeight());
    }
}

Removing JAXB Annotations

If you are using the reference implementation of JAXB you are required to use annotations to map objects to XML. As I mentioned in a previous post, I’m not a fan of using annotations for the purpose of mapping objects to some other format (relational database or XML or A.N. Other). I find it creates code clutter especially in instances were entity model classes are mapped to both a relational database and XML (using JAXB). In modular projects, it also creates unnecessary dependencies on the model module.
Luckily, some bright spark came along and created JAXB Introductions which allows you to define the mapping of objects to XML in an XML file. I’m not going to regurgitate the information on the JAXB Introductions website but I will outline the modifications I had to make to allow me to use JAXB Introductions with the example above.
First, I added the maven dependency to the project.
Listing 7: Jaxb Introductions Maven Dependency
01
02
03
04
05
<dependency>
  <groupId>jboss.jaxbintros</groupId>
  <artifactId>jboss-jaxb-intros</artifactId>
  <version>1.0.1.GA</version>
</dependency>
Second, I created a JAXB Introductions mapping file.
Listing 8: marshaller-mapping.xml
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?xml version = "1.0" encoding = "UTF-8"?>
    <Class name="net.thoughtforge.model.Person">
        <XmlType/>
        <XmlRootElement name="Person" namespace="http://thoughtforge.net/model"/>
        <XmlAccessorType value="FIELD"/>
        <Field name="dateOfBirth">
            <XmlElement name="dateOfBirth" namespace="http://thoughtforge.net/model"/>
        </Field>
        <Field name="firstName">
            <XmlElement name="firstName" namespace="http://thoughtforge.net/model"/>
        </Field>
        <Field name="height">
            <XmlElement name="height" namespace="http://thoughtforge.net/model"/>
        </Field>
        <Field name="lastName">
            <XmlElement name="lastName" namespace="http://thoughtforge.net/model"/>
        </Field>
        <Field name="weight">
            <XmlElement name="weight" namespace="http://thoughtforge.net/model"/>
        </Field>
    </Class>
</jaxb-intros>
Thirdly, I modified the Spring configuration to inject the JAXB Introductions annotation reader into the JAXB marshaller.
Listing 9: applicationContext-marshaller.xml
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?xml version="1.0" encoding="UTF-8"?>
    <context:component-scan base-package="net.thoughtforge.marshaller"/>
    <bean id="jaxb2Marshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
        <property name="classesToBeBound">
            <list>
                <value>net.thoughtforge.model.Person</value>
            </list>
        </property>
        <property name="jaxbContextProperties">
            <map>
                <entry>
                    <key>
                        <util:constant static-field="com.sun.xml.bind.api.JAXBRIContext.ANNOTATION_READER"/>
                    </key>
                    <bean class="org.jboss.jaxb.intros.IntroductionsAnnotationReader">
                        <constructor-arg ref="jaxbIntroductions"/>
                    </bean>
                </entry>
            </map>
        </property>
        <property name="schema" value="classpath:schema/person.xsd"/>
    </bean>
    <bean id="jaxbIntroductions" class="org.jboss.jaxb.intros.IntroductionsConfigParser"
            factory-method="parseConfig">
        <constructor-arg><value>classpath:marshaller-mapping.xml</value></constructor-arg>
    </bean>
</beans>
Finally, I removed the JAXB annotations from the model class (Person.java).
Listing 10: Person.java
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package net.thoughtforge.model;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.Calendar;
public class Person implements Serializable {
    private static final long serialVersionUID = 8465162879793776395L;
    private Calendar dateOfBirth;
    private String firstName;
    private BigDecimal height;
    private String lastName;
    private BigDecimal weight;
    public Calendar getDateOfBirth() {
        return dateOfBirth;
    }
    public void setDateOfBirth(Calendar dateOfBirth) {
        this.dateOfBirth = dateOfBirth;
    }
    public String getFirstName() {
        return firstName;
    }
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
    public BigDecimal getHeight() {
        return height;
    }
    public void setHeight(BigDecimal height) {
        this.height = height;
    }
    public String getLastName() {
        return lastName;
    }
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
    public BigDecimal getWeight() {
        return weight;
    }
    public void setWeight(BigDecimal weight) {
        this.weight = weight;
    }
}
Hopefully, this short example will prove useful for those starting out with JAXB and OXM. For those already familiar with JAXB and OXM, perhaps it has shown you how you can use JAXB without annotations.

No comments:

Post a Comment