So, I was working on a little program to make it easier to configure collections of servos making up the robot I’m working on when I got to the point of figuring out how I was going to persist this information. Although the sum total of configuration is trivial at the moment I have plans to eventually include enough structural information about the robot that a simple textual list of properties would become unwieldy. An XML format seemed like the obvious choice.
I wrote a simple Relax NG schema and was just about to write yet another SAX based parser and Java Writer when I had this sense that I’d been in this situation way too many times before and that there had to be a better way. Processing an in-memory DOM tree and then using Java’s XML processing to read/write it didn’t sound like a whole lot of fun so in the end I thought I’d give JAXB a go. I’d heard about it years ago (it’s not a new Java technology) but not had an excuse to use it before so now seemed like a good time to give it a go.
There are several tutorials and articles about JAXB out there - unfortunately they all seem to cover different versions. JAXB 2.x seems much less hassle to use than JAXB 1.x. Also, while the current version of JAXB is 2.1, the version of JAXB bundled with the Java 1.6 SDK is 2.0 and the bundled version doesn’t have the ant
In order to prevent examples getting too long we’ll stay with a fragment of the complete data structure which consists of servos and a Robot which is simply a collection of servos.
The impression I got from reading the tutorials and documentation is that you need to start with a schema and generate Java classes from there. I’m not a great fan of XML Schema so I decided to stick with my Relax NG schema:
<?xml version="1.0" encoding="UTF-8"?>
<grammar
xmlns="http://relaxng.org/ns/structure/1.0"
xmlns:xsp="http://apache.org/xsp/core/v1"
xmlns:s="http://www.ascc.net/xml/schematron"
xmlns:a="http://relaxng.org/ns/compatibility/annotations/1.0"
xmlns:f="http://axkit.org/NS/xsp/perform/v1"
datatypeLibrary="http://www.w3.org/2001/XMLSchema-datatypes">
<start>
<element name="robot">
<ref name="identifiable-element"/>
<zeroOrMore>
<ref name="servo-element"/>
</zeroOrMore>
</element>
</start>
<define name="identifiable-element">
<attribute name="id"/>
<attribute name="name"/>
</define>
<define name="servo-element">
<element name="servo">
<ref name="identifiable-element"/>
<optional>
<attribute name="min_angle" a:defaultValue="-1.570796327">
<data type="decimal"/>
</attribute>
<attribute name="max_angle" a:defaultValue="1.570796327">
<data type="decimal"/>
</attribute>
</optional>
<optional>
<attribute name="rest_angle" a:defaultValue="0">
<data type="decimal"/>
</attribute>
</optional>
<optional>
<attribute name="min_pulse_width" a:defaultValue="500">
<data type="integer"/>
</attribute>
<attribute name="max_pulse_width" a:defaultValue="2500">
<data type="integer"/>
</attribute>
</optional>
<optional>
<attribute name="max_speed" a:defaultValue="2500">
<data type="integer"/>
</attribute>
</optional>
<optional>
<attribute name="desired_position" a:defaultValue="0">
<data type="integer"/>
</attribute>
</optional>
</element>
</define>
</grammar>
I then used Oxygen XML’s built-in copy of trang to convert this to XML Schema:
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
<xs:element name="robot">
<xs:complexType>
<xs:sequence>
<xs:element minOccurs="0" maxOccurs="unbounded" ref="servo"/>
</xs:sequence>
<xs:attributeGroup ref="identifiable-element"/>
</xs:complexType>
</xs:element>
<xs:attributeGroup name="identifiable-element">
<xs:attribute name="id" use="required"/>
<xs:attribute name="name" use="required"/>
</xs:attributeGroup>
<xs:element name="servo">
<xs:complexType>
<xs:attributeGroup ref="identifiable-element"/>
<xs:attribute name="min_angle" default="-1.570796327" type="xs:decimal"/>
<xs:attribute name="max_angle" default="1.570796327" type="xs:decimal"/>
<xs:attribute name="rest_angle" default="0" type="xs:decimal"/>
<xs:attribute name="min_pulse_width" default="500" type="xs:integer"/>
<xs:attribute name="max_pulse_width" default="2500" type="xs:integer"/>
<xs:attribute name="max_speed" default="2500" type="xs:integer"/>
<xs:attribute name="desired_position" default="0" type="xs:integer"/>
</xs:complexType>
</xs:element>
</xs:schema>
Running xjc from the command line then generated 5 Java classes that would allow me to read/write this format. There was were just a couple of snags:
The generated classes Robot and Servo had no behaviour. In my existing codebase these classes needed behaviour and editing generated code is clearly a bad idea.
My hand written classes used primitive int and double but the generated classes used java.lang.Integer and java.lang.Double. [1]
There is a section in the Unofficial JAXB Guide on adding behaviour to generated classes which suggests that the solution to my first problem was to define classes which extend the generated classes to add the behaviour and then create a custom factory class that makes JAXB instantiate instances of the subclasses instead of the generated classes.
The solution to the second problem seemed to be to use JAXB’s support for customisation to force it to use primitive int and double for the attribute values.
You can customise how JAXB generates classes using annotations in the schema or an external binding file. Since I was generating my schema from Relax NG and, in any case I wanted to keep the schema clean, I elected to go for an external binding file.
I wanted to keep convenient names for my hand-written classes and so I elected to use customisation to both change the data types of the generated classes instance variables and to rename the generated classes to have the suffix "Element." Here is my final attempt at a JAXB binding file to do this:
<jxb:bindings version="1.0"
xmlns:jxb="http://java.sun.com/xml/ns/jaxb"
xmlns:xs="http://www.w3.org/2001/XMLSchema">
<jxb:bindings schemaLocation="robot.xsd" node="/xs:schema">
<jxb:globalBindings>
<jxb:javaType name="int" xmlType="xs:integer"/>
<jxb:javaType name="double" xmlType="xs:decimal" />
</jxb:globalBindings>
<jxb:schemaBindings>
<jxb:package name="org.emptiness.hexapod.autogen.robotmodel">
<jxb:javadoc>
<![CDATA[<body> Package level documentation for generated package org.emptiness.hexapod.autogen.robotmodel.</body>]]>
</jxb:javadoc>
</jxb:package>
<jxb:nameXmlTransform>
<jxb:elementName suffix="Element"/>
</jxb:nameXmlTransform>
</jxb:schemaBindings>
</jxb:bindings>
</jxb:bindings>
Unfortunately, although this did correctly generate classes called RobotElement and ServoElement the element attribute values were still mapped to java.lang.Integer and java.lang.Double - some more googling found this forum post and another one which indicated that JAXB was forcing the use of classes since the attributes were optional and might therefore need to be null. There did not seem to be any way around this and so I reluctantly decided to leave things as they were and be grateful that Java supports autoboxing so that I could get away without having to modify any code using my Servo class.
I created my custom factory:
package org.emptiness.hexapod.core;
import javax.xml.bind.annotation.XmlRegistry;
import org.emptiness.hexapod.autogen.robotmodel.ObjectFactory;
import org.emptiness.hexapod.autogen.robotmodel.RobotElement;
import org.emptiness.hexapod.autogen.robotmodel.ServoElement;
@XmlRegistry
public class RobotObjectFactory extends ObjectFactory {
/**
* Create an instance of {@link RobotElement }
*
*/
public RobotElement createRobotElement() {
System.err.println("RobotElement createRobotElement");
return new Robot();
}
/**
* Create an instance of {@link ServoElement }
*
*/
public ServoElement createServoElement() {
System.err.println("ServoElement createServoElement");
return new Servo();
}
}
But try as I might I could not get JAXB to use to create instances of my classes instead of the ones it had generated. By this time I was thoroughly fed up with JAXB and ready to try something else. A little further reading suggested that the way forward might be to use annotations to mark up my handwritten classes and abandon the whole idea of getting JAXB to generate the data bearing classes. After some messing about I had a new Servo class that looked a little like this (for brevity I’ve removed all the constant declarations, behaviour methods and getters/setters from the code below):
package org.emptiness.hexapod.core;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlSchemaType;
import javax.xml.bind.annotation.XmlTransient;
import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.emptiness.hexapod.core.jaxb.DoubleAdapter;
import org.emptiness.hexapod.core.jaxb.IntegerAdapter;
import org.emptiness.hexapod.device.ServoController;
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "")
@XmlRootElement(name = "servo")
public class Servo implements RobotComponent {
// configuration information
@XmlAttribute(required = true)
@XmlSchemaType(name = "anySimpleType")
private String id; // identifier passed to the controller
@XmlAttribute(required = true)
@XmlSchemaType(name = "anySimpleType")
private String name; // meaningful human name
@XmlAttribute(name = "min_angle")
@XmlJavaTypeAdapter(DoubleAdapter.class)
@XmlSchemaType(name = "decimal")
private Double minAngle; // (radians)
@XmlAttribute(name = "max_angle")
@XmlJavaTypeAdapter(DoubleAdapter.class)
@XmlSchemaType(name = "decimal")
private Double maxAngle; // (radians)
@XmlAttribute(name = "rest_angle")
@XmlJavaTypeAdapter(DoubleAdapter.class)
@XmlSchemaType(name = "decimal")
private Double restAngle; // (radians)
@XmlAttribute(name = "min_pulse_width")
@XmlJavaTypeAdapter(IntegerAdapter.class)
@XmlSchemaType(name = "integer")
private Integer minPulseWidth; // (microseconds)
@XmlAttribute(name = "max_pulse_width")
@XmlJavaTypeAdapter(IntegerAdapter .class)
@XmlSchemaType(name = "integer")
private Integer maxPulseWidth; // (microseconds)
@XmlAttribute(name = "max_speed")
@XmlJavaTypeAdapter(IntegerAdapter .class)
@XmlSchemaType(name = "integer")
private Integer maxSpeed; // (max change in pulse width per second)
// current state
@XmlTransient
private int actualPosition = UNKNOWN_POSITION;
@XmlTransient
private int desiredPosition = UNKNOWN_POSITION;
@XmlTransient
private int speed = DEFAULT_SPEED;
@XmlTransient
private ServoController controller = null;
}
Since I was now annotating classes which contained state that should not be persisted I had to use @XmlTransient to prevent JAXB from attempting to persist those instance variables.
The Robot class now looked like this (again with large chunks of code removed):
package org.emptiness.hexapod.core;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlSchemaType;
import javax.xml.bind.annotation.XmlType;
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {
"servo"
})
@XmlRootElement(name = "robot")
public class Robot {
protected List<Servo> servo;
@XmlAttribute(required = true)
@XmlSchemaType(name = "anySimpleType")
protected String id;
@XmlAttribute(required = true)
@XmlSchemaType(name = "anySimpleType")
protected String name;
public Robot() {
}
// file operations
public static Robot load(File inputFile) throws Exception {
//JAXBContext context = JAXBContext.newInstance(ObjectFactory.class);
JAXBContext context = JAXBContext.newInstance(RobotObjectFactory.class);
Unmarshaller u = context.createUnmarshaller();
//u.setProperty("com.sun.xml.bind.ObjectFactory",new RobotObjectFactory());
Robot robot = (Robot) u.unmarshal(inputFile);
return robot;
}
public void save(File ouptutFile) throws Exception {
JAXBContext context = JAXBContext.newInstance(RobotObjectFactory.class);
Marshaller m = context.createMarshaller();
m.setProperty("jaxb.formatted.output", Boolean.TRUE);
OutputStream os = new FileOutputStream(ouptutFile);
m.marshal(this, os);
}
public List<Servo> getServos() {
return getServo();
}
public List<Servo> getServo() {
if (servo == null) {
servo = new ArrayList<Servo>();
}
return this.servo;
}
}
Since the servo element is called "servo" JAXB expects the getter methods for accessing the list to be called "getServo" - I, however, felt the method was more naturally called "getServos" and this was what I used in my existing code. I therefore created a wrapper method to allow me to continue using "getServos" The load() and save() methods above show the small amount of code actually required to invoke JAXB to read or write the data from/to a file.
So, I now have the ability to read and write my data to XML without having had to write yet more code to implement a SAX interface or generate XML with PrintWriter.println() but getting things working with JAXB took me far longer than the "SAX + println()" approach would have done and I can’t say that I’m 100% happy with the final result. I may eventually try another approach like Xstream and see if that gives better results with less overall hassle.
Links
Official JAXB home page
Wikipedia page on JAXB
FAQ entry on adding behaviours to JAXB generated classes
A practical guide to JAXB 2.0 (The Register)
XML annotations javadoc
Xstream
Comments
I've been having a look in to
I've been having a look in to some Object/XML mapping stuff myself recently and ended up choosing JiBX over JAXB just because there seemed to be much better documentation that there was for JAXB and also I was able to write the mapping file myself and map the classes to my pre-existing POJOs.
I think JAXB fell out of favour with me because when visiting the official site of a project, you don't want to end up reading the "Unofficial User Guide".
@ Phil, I'll take a look at
@ Phil, I'll take a look at JiBX - thanks for the pointer. XStream has also been recommended to me as well.
Reply to comment | Dave Snowdon
I don't even know how I ended up here, but I thought this post was great. I don't
know who you are but definitely you are going to a famous blogger if you are
not already ;) Cheers!
int and double
Try minOccurs="1" to achieve the primitives.
Post new comment