Using JAXB Annotations to read/write XML

Mon, 03 Jan 2011 18:31:51 -0800
Scheme, kawa

The Java Architecture for XML Binding (JAXB) 2.0 lets you use annotations to how specify Java class instances get printed (serialized) as XML documents. Kawa recently gained support for annotations, so let us look at an example. You will need Java 6 for the JAXB 2.0 support, and Kawa from SVN (or Kawa 1.12 when it comes out). Nothing else is needed.
Our example is a simple bibliography database of books and some information about them. The DTD and sample input data are taken from the XML Query Use Cases. The complete example is in the file testuite/jaxb-annotations3.scm in the Kawa source distribution.
Defining the Java classes and their XML mapping
We start with some define-alias declarations, which serve the role of Java's import:

(define-alias JAXBContext javax.xml.bind.JAXBContext)
(define-alias StringReader java.io.StringReader)
(define-alias XmlRegistry javax.xml.bind.annotation.XmlRegistry)
(define-alias XmlRootElement javax.xml.bind.annotation.XmlRootElement)
(define-alias XmlElement javax.xml.bind.annotation.XmlElement)
(define-alias XmlAttribute javax.xml.bind.annotation.XmlAttribute)
(define-alias BigDecimal java.math.BigDecimal)
(define-alias ArrayList java.util.ArrayList)

The bibliography is represented by a Java singleton class Bib, which has a books field, which is an ArrayList collection of Book objects:

(define-simple-class Bib ( ) (@XmlRootElement name: "bib")
  (books (@XmlElement name: "book" type: Book) ::ArrayList))

The XmlRootElement annotation says that the Bib is represented in XML as a root element (document element) named <bib>. Each Book is represented using a <book> element. Note the annotation value name: "book" is needed because I decided to use the plural books for the field name (because it is an List), but the singular book for the tag name for each book element.
Each book is represented using the class Book, which has the fields year, title, publisher (all strings), authors and editors (both ArrayLists), and price (a BigDecimal). Note that the while the other fields get mapped to XML child elements, the year is mapped to an XML attribute, also named year.

(define-simple-class Book ()
  (year (@XmlAttribute required: #t) ::String)
  (title (@XmlElement) ::String)
  (authors (@XmlElement name: "author" type: Author) ::ArrayList)
  (editors (@XmlElement name: "editor" type: Editor) ::ArrayList)
  (publisher (@XmlElement) ::String)
  (price (@XmlElement) ::BigDecimal))

Finally, the Author and Editor classes, both of which inherit from Person:

(define-simple-class Person ()
  (last (@XmlElement) ::String)
  (first (@XmlElement) ::String))

(define-simple-class Author (Person))

(define-simple-class Editor (Person)
  (affiliation (@XmlElement) ::String))

Setting up the JAXB context

Next we need to specify a JAXBContext, which manages the mapping between Java classes and the XML document schema (structure). Once JAXB knows the classes involved, it can figure out the XML representation by analyzing the classes and their annotations (using reflection). A simple way to do this is to just list the needed classes when creating the JAXBContext:

(define jaxb-context ::JAXBContext
  (JAXBContext:newInstance Bib Book Person Editor))

(Actually, you don't need to list all the classes: if you just list the root class Bib, JAXB can figure out the rest.)
An alternative is to make use of an ObjectFactory. Specifically, if the element classes are in a package named my.package then you need to create a class ObjectFactory (in the same package), and specify the package name to the JAXBContext factory method newInstance. The ObjectFactory class needs to have a factory method for at least the root class Bib:

(define jaxb-context ::JAXBContext
  (JAXBContext:newInstance "my.package"))
(define-simple-class ObjectFactory () (@XmlRegistry)
  ((createBib) ::Bib (Bib)))

Unmarshalling - reading XML and creating objects

At this point actually parsing the XML and creating objects is trivial. You create an Unmarshaller from the JAXBContext, and then just invoke one of the Unmarshaller unmarshall methods.

(define (parse-xml (in ::java.io.Reader)) ::Bib
  ((jaxb-context:createUnmarshaller):unmarshal in))
(define bb (parse-xml (current-input-port)))

Update the values

The whole point of unmarshalling the XML is presumably so you can do something with the data. First, an example of modifying existing records: let's deal with inflation and increase all the prices by 10%:

;; Multiply the price of all the books in bb by ratio.
(define (adjust-prices (bb::Bib) (ratio::double))
  (let* ((books bb:books)
         (nbooks (books:size)))
    (do ((i :: int 0 (+ i 1))) ((>= i nbooks))
      (let* ((book ::Book (books i)))
        (set! book:price (adjust-price book:price ratio))))))

;; Multiply old by ratio to yield an updated 2-decimal-digit BigDecimal.
(define (adjust-price old::BigDecimal ratio::double)::BigDecimal
  (BigDecimal (round (* (old:doubleValue) ratio 100)) 2))

(adjust-prices bb 1.1)

Next, let's add a new book:

(bb:books:add
 (Book year: "2006"
       title: "JavaScript: The Definitive Guide (5th edtion)"
       authors: [(Author last: "Flanagan" first: "David")]
       publisher: "O'Reilly"
       price: (BigDecimal "49.99")))

Notice the use of square brackets (a new Kawa feature) for the authors single-element sequence.
Marshalling - writing XML from objects

Finally, we might want to write the updated data out to an XML file. For that we create a Marshaller. It has a number of marshall methods that take a jaxbElement and a target specification, and serializes the former to the latter as XML. For example, you could just pass in a File to specify the name of the XML output file. However, Kawa includes an XML pretty-printer, and it would be nice to have pretty-printed and indented XML. To do that we have to use a SAX2 ContentHandler as the bridge between JAXB and Kawa's XML tools. The Kawa XMLFilter implements ContentHandler and can forward XML-like data to an XMLPrinter. The latter doesn't do pretty-printing by default - you have to set the *print-xml-indent* fluid variable to the symbol 'pretty.

;; Write bb as pretty-printed XML to an output port.
(define (write-bib (bb ::Bib) (out ::output-port))::void
  (let ((m (jaxb-context:createMarshaller)))
    (fluid-let ((*print-xml-indent* 'pretty))
      ;; We could marshal directly to 'out' (which extends java.io.Writer).
      ;; However, XMLPrinter can pretty-print the output more readably.
      ;; We use XMLFilter as glue that implements org.xml.sax.ContentHandler.
      (m:marshal bb (gnu.xml.XMLFilter (gnu.xml.XMLPrinter out))))))

(write-bib bb (current-output-port))