JavaBeans to XML, With No Libraries
Join the DZone community and get the full member experience.
Join For FreeConverting JavaBeans to XML and viceversa is quite a common task, and there are tons of libraries around for this purpose. But I always like to use what is already available in the JRE, avoiding dependencies as much as possible. In the past I and Simone developed a rest API using the JSR57 serialization, that was already available in the Java 5. But Java 6 included JAXB in the standard libraries, which is more modern and flexible, and gives you a better control over the XML translation process.
Here I want to provide a couple of simple examples to show how those two APIs work.
Suppose we have some java beans "the model" as per the following source:
public class Book implements Serializable {Two simple JavaBeans, a classic example: one models a book, with title, author and price. And the price is composed by the amount and the currency. The Currency class with its hidden partner CurrencyData class are a funny couple in the JDK; if you look at the code you may find it quite amusing; I suggest to never use that. By the way, it is a good example here, because that class other than having such source code, doesn't have a public default constructor, and makes the XML marshaling fail. So it makes a good example for a not so trivial XML serialization.
String title;
String author;
Price price;
public Book() {} //default constructor is mandatory for JavaBeans
public Book(String title, String author, Price price) {
this.title= title;
this.author = author;
this.price = price;
}
//... imagine the getter and setters boilerplate code here ...
@Override
public String toString() {
return title + " by " + author + ", " + price;
}
}
public class Price implements Serializable {
Double amount;
Currency currency;
public Price() {} // default constructor, as JavaBeans mandate...
public Price(Double amount, Currency currency) {
this.amount = amount;
this.currency = currency;
}
//... imagine the getter and setters boilerplate code here ...
@Override
public String toString() {
DecimalFormat fmt = new DecimalFormat("0.00");
return fmt.format(amount) + currency;
}
}
The following code samples will instantiate a Book object, as per above class definitions, then transform that to an XML String, and after the XML String will be used to reconstruct another instance of the Book object.
JSR57 Serializer. Also known as "XMLDecoder/XMLEncoder"
Let's start with the JSR57 Serializer.
public class Jsr57Spike {
public static void main(String[] args) throws Exception {
Book book = new Book("Carrie", "Stephen King", new Price(17.25,
Currency.getInstance("CHF")));
String xml = encode(book);
System.out.println(xml);
Object o = decode(xml);
System.out.println("decoded object: " + o);
}
private static String encode(Book book) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
XMLEncoder encoder = new XMLEncoder(out);
fixCurrency(encoder);
encoder.writeObject(book);
encoder.close();
return out.toString();
}
private static void fixCurrency(XMLEncoder encoder) {
encoder.setPersistenceDelegate(Currency.class,
new PersistenceDelegate() {
@Override
protected Expression instantiate(Object oldInstance,
Encoder out) {
return new Expression(Currency.class, "getInstance",
new Object[] { oldInstance.toString() });
}
});
}
private static Object decode(String xml)
throws UnsupportedEncodingException {
XMLDecoder decoder = new XMLDecoder(new ByteArrayInputStream(xml
.getBytes("UTF-8")));
return decoder.readObject();
}
}
As you can see, I had to write a special handler to deal with the java.util.Currency class (lines 25..33), the PersistencyDelegate for the Currency class has to be set on the encoder before usage. If you don't do it, it will not throw any exception, but the decoder will be unable to deserialize the XML as it will fail to instantiate the Currency class. It will not throw any exception, it will just print on System.error some weird stuff like:
java.lang.InstantiationException: java.util.Currency Continuing ... java.lang.RuntimeException: failed to evaluate: <unbound>=Class.new(); Continuing ...
And the decoded object will miss the currency instances. You can easily try that commenting the line number 17, where the "fix" is applied.
With the fix in place, everything should go fine and produce the following output:
<?xml version="1.0" encoding="UTF-8"?>
<java version="1.6.0_20" class="java.beans.XMLDecoder">
<object class="it.newinstance.xml.spike.model.Book">
<void property="author">
<string>Stephen King</string>
</void>
<void property="price">
<object class="it.newinstance.xml.spike.model.Price">
<void property="amount">
<double>17.25</double>
</void>
<void property="currency">
<object class="java.util.Currency" method="getInstance">
<string>CHF</string>
</object>
</void>
</object>
</void>
<void property="title">
<string>Carrie</string>
</void>
</object>
</java>
decoded object: Carrie by Stephen King, 17.25CHF
We may find that the above XML representation is possibly too verbose and too Java-centric. So we may look forward to Jaxb...
But it's nice to notice here that in this case I didn't have to touch the model (Book and Price class) as the XMLDecoder/Encoder are really not intrusive and may be suitable for JavaBeans from which we do not have the source code or we can't easily change.
JAXB: Java Architecture for XML Binding
Following code does again the same stuff: from a JavaBean to XML and the way back.
public class JaxbSpike {
@XmlRootElement
private static class XMLBook extends Book {
@SuppressWarnings("unused")
public XMLBook() {} // default constructor is mandated by JavaBeans spec
public XMLBook(String title, String author, Price price) {
super(title, author, price);
}
}
public static void main(String[] args) throws JAXBException {
JAXBContext jc = JAXBContext.newInstance(XMLBook.class);
Book book = new XMLBook("Carrie", "Stephen King", new Price(17.25,
Currency.getInstance("EUR")));
String xml = marshall(jc, book);
System.out.println(xml);
Book unmashalledBook = unmarshall(jc, xml);
System.out.println("Last book I read: " + unmashalledBook);
}
private static Book unmarshall(JAXBContext jc, String xml)
throws JAXBException {
Unmarshaller u = jc.createUnmarshaller();
return (Book) u.unmarshal(new StringReader(xml));
}
private static String marshall(JAXBContext jc, Book book)
throws JAXBException, PropertyException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
Marshaller m = jc.createMarshaller();
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
m.marshal(book, out);
return out.toString();
}
}
Jaxb requires that the object which represents the XML root has to be annotated with the @XmlRootElement. As I don't like to change my model just to transform it, I preferred in this example to subclass Book and apply the XML serialization to the inner class XMLBook where I can add the annotation without any problem.
At line 34 I set the JAXB_FORMATTED_OUTPUT property to true to produce a formatted XML. This is helpful for human reading, but it is good to have the ability to produce an XML on a single line to optimize the fruition by machines.
The rest of the code is quite self explanatory.
Here, I didn't had to add any handler for the Currency class in the marshaller/unmarshaller code... But I had to declare the converter with an annotation applied to the package. To annotate the package containing the model classes you need to create a special file named "package-info.java" with following source:
@XmlJavaTypeAdapter(value=CurrencyAdapter.class,type=Currency.class)
package it.newinstance.xml.spike.model;
import java.util.Currency;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
The marshaller/unmarshaller will check the annotations applied to the classes, the fields, and the packages, and apply the specified rules. JAXB has a rich set of annotations that can be applied to your JavaBeans and the packages themselves. The problem is that annotations are, under all aspects, interface elements. Annotating a class means introducing dependencies and changing the interfaces of your domain model. Also take in mind that sometime you just don't own the source code of the JavaBeans you want to serialize or you cannot change them. Subclassing may help, as I showed in the above example, introducing the XMLBook inner class, but it may be not enough. Further investigation of the JAXB API may show solutions to this issue, but at the moment, I don't know...
Then I had also to create a very simple CurrencyAdapter class, which will be used by JAXB to handle the translations Java <-> XML
public class CurrencyAdapter extends XmlAdapter<String, Currency>{
@Override
public String marshal(Currency v) throws Exception {
return v.toString();
}
@Override
public Currency unmarshal(String v) throws Exception {
return Currency.getInstance(v);
}
}
If you don't set up the CurrencyAdapter, the Exception that you will get is the following:
Exception in thread "main" com.sun.xml.internal.bind.v2.runtime.IllegalAnnotationsException: 1 counts of IllegalAnnotationExceptions java.util.Currency does not have a no-arg default constructor. this problem is related to the following location: at java.util.Currency at public java.util.Currency it.newinstance.xml.spike.model.Price.getCurrency() at it.newinstance.xml.spike.model.Price at public it.newinstance.xml.spike.model.Price it.newinstance.xml.spike.model.Book.getPrice() at it.newinstance.xml.spike.model.Book at it.newinstance.xml.spike.jaxb.JaxbSpike$XMLBook at com.sun.xml.internal.bind.v2.runtime.IllegalAnnotationsException$Builder.check(IllegalAnnotationsException.java:91) at com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl.getTypeInfoSet(JAXBContextImpl.java:436) at com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl.<init>(JAXBContextImpl.java:277) at com.sun.xml.internal.bind.v2.runtime.JAXBContextImpl$JAXBContextBuilder.build(JAXBContextImpl.java:1100) at com.sun.xml.internal.bind.v2.ContextFactory.createContext(ContextFactory.java:143) at com.sun.xml.internal.bind.v2.ContextFactory.createContext(ContextFactory.java:110) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25) at java.lang.reflect.Method.invoke(Method.java:597) at javax.xml.bind.ContextFinder.newInstance(ContextFinder.java:202) at javax.xml.bind.ContextFinder.find(ContextFinder.java:376) at javax.xml.bind.JAXBContext.newInstance(JAXBContext.java:574) at javax.xml.bind.JAXBContext.newInstance(JAXBContext.java:522) at it.newinstance.xml.spike.jaxb.JaxbSpike.main(JaxbSpike.java:32)
I have to say that the exception is very descriptive of the problem. JAXB, for the little I had the chance to see, is a very well made API.
So, now that we have all in place, let's see what's the output of our JaxbSpike:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<xmlBook>
<author>Stephen King</author>
<price>
<amount>17.25</amount>
<currency>EUR</currency>
</price>
<title>Carrie</title>
</xmlBook>
Last book I read: Carrie by Stephen King, 17.25EUR
For sure a better, simpler and non-Java-centric representation of the Book object, if we compare this with the output given using the XMLEncoder.
But, we have to notice that we have introduced the annotation on the package, so we changed the original domain. It is possible that, learning better the JAXB APIs, this change may be avoided, but I am really not confident in that. For sure JAXB is the best choice when you have some flexibility in changing the code of your JavaBeans.
Want to try by yourself?
Here you find the source code: JavaBeansToXml.tar.gz
Good luck!
P.S. This is all I know about JAXB and JSR57 XML serialization; if you have a specific problem it's better to refer to some community forum. But feel free to leave a comment, I will be happy to help if I know the answer.
From http://en.newinstance.it/2010/08/05/javabeans-to-xml-with-no-libraries/
Opinions expressed by DZone contributors are their own.
Comments