Using JAXB Annotations to read/write XML
The Java Architecture for XML Binding (JAXB) 2.0 lets you use annotations to how specify Java class instances getprinted(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 somedefine-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 ArrayList
s),
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))