|
This page provides a tutorial that will prepare users for using XML serialization. Before
this tutorial is attempted it is advisable to have a look at the Javadoc
documentation for the framework. Although there are only several annotations and objects involved in
the serialization process the framework itself has many powerful features which this tutorial attempts to
describe.
- Serializing a simple object
- Deserializing a simple object
- Nested object serialization
- Optional elements and attributes
- Reading a list of elements
- Overriding an annotated type
- Dealing with an inline list of elements
- Constructor injection
- Reading an array of elements
- Adding text and attributes to elements
- Dealing with map objects
- Scattering inline element entries
- Loose object mapping
- Java Bean serialization
- Example using template filters
- Receiving persister callbacks
- Maintaining state between persister callbacks
- Serializing with CDATA blocks
- Resolving object reference cycles
- Reusing XML elements
- Using utility collections
- Object substitution
- Serializing Java language types
- Styling serialized XML
- Version tolerant serialization
- Serializing static final fields
Serializing a simple object
In order to serialize an object to XML a series of annotations must be placed within that object. These
annotations tell the persister how the object should be serialized. For example take the class shown
below. Here there are three different annotations, one used to describe the name of the root element, one
that describes an XML message element, and a final annotation for an id attribute.
@Root
public class Example {
@Element
private String text;
@Attribute
private int index;
public Example() {
super();
}
public Example(String text, int index) {
this.text = text;
this.index = index;
}
public String getMessage() {
return text;
}
public int getId() {
return index;
}
}
To serialize an instance of the above object a Persister is required.
The persister object is then given an instance of the annotated object and an output result, which is a file in this example. Other
output formats are possible with the persister object.
Serializer serializer = new Persister();
Example example = new Example("Example message", 123);
File result = new File("example.xml");
serializer.write(example, result);
Once the above code is executed the object instance will have been transferred as an XML document to the specified file. The resulting XML
file will contain the contents shown below.
<example index="123">
<text>Example message</text>
</example>
As well as the capability of using the field an object name to acquire the XML element and attribute names explicit naming is possible.
Each annotation contains a name attribute, which can be given a string providing the name of the XML attribute or element. This ensures
that should the object have unusable field or method names they can be overridden, also if your code is obfuscated explicit naming is
the only reliable way to serialize and deserialize objects consistently. An example of the previous object with explicit naming is shown
below.
@Root(name="root")
public class Example {
@Element(name="message")
private String text;
@Attribute(name="id")
private int index;
public Example() {
super();
}
public Example(String text, int index) {
this.text = text;
this.index = index;
}
public String getMessage() {
return text;
}
public int getId() {
return index;
}
}
For the above object the XML document constructed from an instance of the object results in a different format. Here the XML element
and attribute names have been overridden with the annotation names. The resulting output is shown below.
<root id="123">
<message>Example message</message>
</root>
Deserializing a simple object
Taking the above example object the XML deserialization process is described in the code snippet shown below. As can be seen the
deserialization process is just as simple. The persister is given the class representing the serialized object and the source
of the XML document. To deserialize the object the read method is used, which produces an instance of the annotated object. Also,
note that there is no need to cast the return value from the read method as the method is generic.
Serializer serializer = new Persister();
File source = new File("example.xml");
Example example = serializer.read(Example.class, source);
Nested object serialization
As well as simple object serialization, nested object serialization is possible. This is where a serializable object can contain
any number of serializable objects, to any depth. Take the example shown in the code snippet below. This shows several objects
that are linked together to form a single serializable entity. Here the root configuration object contains a server object, which
in turn contains a security information object.
@Root
public class Configuration {
@Element
private Server server;
@Attribute
private int id;
public int getIdentity() {
return id;
}
public Server getServer() {
return server;
}
}
public class Server {
@Attribute
private int port;
@Element
private String host;
@Element
private Security security;
public int getPort() {
return port;
}
public String getHost() {
return host;
}
public Security getSecurity() {
return security;
}
}
public class Security {
@Attribute
private boolean ssl;
@Element
private String keyStore;
public boolean isSSL() {
return ssl;
}
public String getKeyStore() {
return keyStore;
}
}
In order to create an initialized configuration object an XML document can be used. This XML document needs to
match the XML annotations for the object graph. So taking the above class schema the XML document would look
like the following example.
<configuration id="1234">
<server port="80">
<host>www.domain.com</host>
<security ssl="true">
<keyStore>example keystore</keyStore>
</security>
</server>
</configuration>
How the mapping is done can be seen by examining the XML document elements and attributes and comparing these
to the annotations within the schema classes. The mapping is quite simple and can be picked up and understood
in several minutes.
Optional elements and attributes
At times it may be required to have an optional XML element or attribute as the source XML may not contain
the attribute or element. Also, it may be that an object field is null and so cannot be serialized. In
such scenarios the element or attribute can be set as not required. The following code example demonstrates
an optional element and attribute.
@Root
public class OptionalExample {
@Attribute(required=false)
private int version;
@Attribute
private String id;
@Element(required=false)
private String name;
@Element
private String address;
public int getId() {
return id;
}
public int getVersion() {
return version;
}
public String getName() {
return name;
}
public String getAddress() {
return address;
}
}
For the above object the version and name are not required. So, and XML source document may not contain either of
these details and the object can still be serialized safely. For example take the following XML document, which
is a valid representation of the above object.
<optionalExample id='10'>
<address>Some example address</address>
</optionalExample>
Even without the name and version XML nodes this document can be deserialized in to an object. This feature is
useful when your XML contains optional details and allows more flexible parsing. To further clarify the implementation
of optional fields take the example shown below. This shows how the entry object is deserialized from the above document,
which is contained within a file. Once deserialized the object values can be examined.
Serializer serializer = new Persister();
File source = new File("example.xml");
OptionalExample example = serializer.read(OptionalExample.class, source);
assert example.getVersion() == 0;
assert example.getName() == null;
assert example.getId() == 10;
Reading a list of elements
In XML configuration and in Java objects there is often a one to many relationship from a parent to
a child object. In order to support this common relationship an ElementList annotation has been
provided. This allows an annotated schema class to be used as an entry to a Java collection object.
Take the example shown below.
@Root
public class PropertyList {
@ElementList
private List<Entry> list;
@Attribute
private String name;
public String getName() {
return name;
}
public List getProperties() {
return list;
}
}
@Root
public class Entry {
@Attribute
private String key;
@Element
private String value;
public String getName() {
return name;
}
public String getValue() {
return value;
}
}
From the above code snippet the element list annotation can be seen. The field type is reflectively instantiated
as a matching concrete object from the Java collections framework, typically it is an array list, but can be
any collection object if the field type declaration provides a concrete implementation type rather than the
abstract list type shown in the above example.
Below an XML document is shown that matches the schema class. Here each entry element will be deserialized
using the declared entry class and inserted into the collection instance created. Once all entry objects have
been deserialized the object instance contains a collection containing individual property objects.
<propertyList name="example">
<list>
<entry key="one">
<value>first value</value>
</entry>
<entry key="two">
<value>first value</value>
</entry>
<entry key="three">
<value>first value</value>
</entry>
<entry key="four">
<value>first value</value>
</entry>
</list>
</propertyList>
From the above example it can be seen that the entry details are taken from the generic type of the collection.
It declares a list with the entry class as its generic parameter. This type of declaration is often not possible,
for example if a specialized list contains more than one generic type which one is the correct type to use for
deserialization or serialization. In such scenarios the type must be provided explicitly. Take the following example.
@Root
public class ExampleList {
@ElementList(type=C.class)
private SpecialList<A, B, C> list;
public SpecialList<A, B, C> getSpecialList() {
return list;
}
}
In the above example the special list takes three generic parameters, however only one is used as the generic parameter
for the collection. As can be seen an explicit declaration of which type to use is required. This can be done with the
type attribute of the ElementList annotation.
Overriding an annotated type
In order to accommodate dynamic types within the deserialization process a class attribute can be added to an
XML element, which will ensure that that element can be instantiated as the declared type. This ensures that
field and method types can reference abstract classes and interfaces, it also allows multiple types to be added into
an annotated collection.
package example.demo;
public interface Task {
public double execute();
}
@Root
public class Example implements Task {
@Element
private Task task;
public double execute() {
return task.execute();
}
}
public class DivideTask implements Task {
@Element(name="left")
private float text;
@Element(name="right")
private float right;
public double execute() {
return left / right;
}
}
public class MultiplyTask implements Task {
@Element(name="first")
private int first;
@Element(name="second")
private int second;
public double execute() {
return first * second;
}
}
The class attribute must be a fully qualified class name so that the context class loader can load
it. Also, the type can contain its own unique annotations and types which makes the deserialization
and serialization process truly dynamic. Below is an example XML document declaring the class
type for the task object.
<example>
<task class="example.demo.DivideTask">
<left>16.5</left>
<right>4.1</right>
</task>
</example>
In order to execute the task described in the XML document the following code can be used. Here it is
assumed the XML source is contained within a file. Once the example object has been deserialized
the task can be executed and the result acquired.
Serializer serializer = new Persister();
File example = new File("example.xml");
Example example = serializer.read(Example.class, example)
double value = example.execute();
Dealing with an inline list of elements
When dealing with third party XML or with XML that contains a grouping of related elements a common format involves
the elements to exist in a sequence with no wrapping parent element. In order to accomodate such structures the
element list annotation can be configured to ignore the parent element for the list. For example take the following
XML document.
<propertyList>
<name>example</name>
<entry key="one">
<value>first value</value>
</entry>
<entry key="two">
<value>second value</value>
</entry>
<entry key="three">
<value>third value</value>
</entry>
</propertyList>
In the above XML document there is a sequence of entry elements, however unlike the previous example these are not
enclosed within a parent element. In order to achieve this the inline attribute of the
ElementList annotation can be set to true.
The following code snippet demonstrates how to use the inline attribute to process the above XML document.
@Root
public class PropertyList {
@ElementList(inline=true)
private List<Entry> list;
@Element
private String name;
public String getName() {
return name;
}
public List getProperties() {
return list;
}
}
There are a number of conditions for the use of the inline element list. Firstly, each element within the inline
list must be placed one after another. They cannot be dispersed in between other elements. Also, each entry
type within the list must have the same root name, to clarify take the following example.
package example.demo;
@Root
public class Entry {
@Attribute
protected String key;
@Element
protected String value;
public String getKey() {
return key;
}
}
public class ValidEntry extends Entry {
public String getValue() {
return value;
}
}
@Root
public class InvalidEntry extends Entry {
public String getValue() {
return value;
}
}
@Root(name="entry")
public class FixedEntry extends InvalidEntry {
}
All of the above types extend the same base type, and so all are candidates for use with the PropertyList
described earlier. However, although all types could be successfully deserialized and serialized using a list
which is not inline, only some can be serialized with an inline list. For instance the type InvalidEntry
could not be serialized as it will be serialized with a different name from all the other entrie implementations.
The InvalidEntry object has a Root annotation
which means that its XML element name will be "invalidEntry". In order to be used with the inline list all objects must
have the same XML element name of "entry". By extending the InvalidEntry type and explicitly specifying the
name to be "entry" the FixedEntry subclass can be used without any issues. For example take the following
XML document, which could represent a mixture of entry types.
<propertyList>
<name>example</name>
<entry key="one" class="example.demo.ValidEntry">
<value>first value</value>
</entry>
<entry key="two" class="example.demo.FixedEntry">
<value>second value</value>
</entry>
<entry key="three" class="example.demo.Entry">
<value>third value</value>
</entry>
</propertyList>
All of the above entry elements within the inline list contain the same XML element name. Also each type is specified
as a subclass implementation of the root Entry object.
Constructor injection
All but the simplest of programs will have some form of immutable objects. These are objects that
do not have setters and so will acquire data by using constructor injection. In this manner the
object sets its internal state from the data provided to the constructor. This can also be achieved
with serialization, if you would like to serialize and deserialize objects but do not want to
provide setter methods this can be done, as illustrated in the example below.
@Root
public class OrderManager {
private final List<Order> orders;
public OrderManager(@ElementList(name="orders") List<Order> orders) {
this.orders = orders;
}
@ElementList(name="orders")
public List<Order> getOrders() {
return orders;
}
}
@Root
public class Order {
@Attribute(name="name")
private final String name;
@Element(name="product")
private final String product;
public Order(@Attribute(name="name") String name, @Element(name="product") String product) {
this.product = product;
this.name = name;
}
public String getProduct() {
return product;
}
}
The above code illustrates an order manager that contains a list of immutable order
objects. On deserialization the values are taken from the XML document and injected
in to the constructor to instantiate the object. This is a very useful feature that
is not often found in serialization frameworks. One restriction on the constructor
injection is that it must be used with an annotated get method or field. This is required
so that on serialization the persister knows where to get the data to write. Taking
the above example if the getOrders method was not annotated then there
would be no way to determine how to write the order manager object.
Below is some example XML resulting from serialization of the order manager.
<orderManager>
<order name="AX101">
<product>Product A</product>
</order>
<order name="AX102">
<product>Product B</product>
</order>
<order name="AX103">
<product>Product C</product>
</order>
</orderManager>
Reading an array of elements
As well as being able to deserialize elements in to a collection arrays can also be serialized and deserialized.
However, unlike the @ElementList annotation the ElementArray
annotation can also deserialize primitive values such as int arrays, char arrays, and
so on. Below is an example object with an array of integer values and a parallel array of string values.
@Root
public class AddressBook {
@ElementArray
private Address[] addresses;
@ElementArray
private String[] names;
@ElementArray
private int[] ages;
public Address[] getAddresses() {
return addresses;
}
public String[] getNames() {
return names;
}
public int[] getAges() {
return ages;
}
}
@Root
public class Address {
@Element(required=false)
private String house;
@Element
private String street;
@Element
private String city;
public String getHouse() {
return house;
}
public String getStreet() {
return street;
}
public String getCity() {
return city;
}
}
For the above object both primitive arrays require an entry attribute, this is because primitives can not be
annotated with the Root annotation.
The entry attribute tells the persister than an extra XML element is required to wrap the entry. This
entry element can also be applied to serializable objects that have the Root annotation,
however it is typically only used for primitive arrays. The following XML is an example of what is
produced by the above objects.
<addressBook>
<addresses length='3'>
<address>
<house>House 33</house>
<street>Sesame Street</street>
<city>City</city>
</address>
<address>
<street>Some Street</street>
<city>The City</city>
</address>
<address>
<house>Another House</house>
<street>My Street</street>
<city>Same City</city>
</address>
</addresses>
<names length='3'>
<string>Jonny Walker</string>
<string>Jack Daniels</string>
<string>Jim Beam</string>
</names>
<ages length='3'>
<int>30</int>
<int>42</int>
<int>31</int>
</ages>
</properties>
Looking at the above XML it can be seen that each entity within an array index is named the same as its type. So
a string is wrapped in a 'string' element and an int is wrapped in an 'int' element. This is done
because the default name for the ElementArray
annotation is its type name, unless the Root annotation
is used with a name. This can be overridden by providing an explicit entry name for the array. For example take the simple object
below which contains an array of names as string objects.
@Root
public class NameList {
@ElementArray(entry="name")
private String[] names;
public String[] getNames() {
return names;
}
}
For the above XML the following document is a valid representation. Notice how each of the names within the XML
document is wrapped in a 'name' element. This element name is taken from the annotation provided.
<nameList>
<names length='3'>
<name>Jonny Walker</name>
<name>Jack Daniels</name>
<name>Jim Beam</name>
</names>
</nameList>
Adding text and attributes to elements
As can be seen from the previous example annotating a primitive such as a String with the
Element annotation will
result in text been added to a names XML element. However it is also possible to add text
to an element that contains attributes. An example of such a class schema is shown below.
@Root
public class Entry {
@Attribute
private String name;
@Attribute
private int version;
@Text
private String value;
public int getVersion() {
return version;
}
public String getName() {
return name;
}
public String getValue() {
return value;
}
}
Here the class is annotated in such a way that an element contains two attributes named version and name. It
also contains a text annotation which specifies text to add to the generated element. Below is an example
XML document that can be generated using the specified class schema.
<entry version='1' name='name'>
Some example text within an element
</entry>
The rules that govern the use of the Text
annotation are that there can only be one per schema class. Also, this annotation cannot be used with the
Element annotation. Only the Attribute
annotation can be used with it as this annotation does not add any content within the owning element.
Dealing with map objects
Although it is possible to deal with most repetitive XML elements within documents using element lists it is often more convenient to use a Map object. In order to deal with maps the ElementMap annotation can be used. The element map annotation can be used with both primitive and composite objects. For example take the following XML document.
<properties>
<property key="one">first value</property>
<property key="two">second value</property>
<property key="three">third value</property>
<name>example name</name>
</properties>
In the above XML document the sequence of properties elements can be used to describe a map of strings, where the key attribute acts as the key for the value within the property element. The following code snipped demonstrates how to use the ElementMap annotation to process the above XML document.
@Root(name="properties")
public class PropertyMap {
@ElementMap(entry="property", key="key", attribute=true, inline=true)
private Map<String, String> map;
@Element
private String name;
public String getName() {
return name;
}
public Map<String, Entry> getMap() {
return map;
}
}
Scattering inline element entries
Elements that are scattered throughout an XML document can be collected by inline lists and inline maps. Simply provide
an entry name for the XML element name the list or map is to collect and they will be extracted and placed in to
the collection object. For example take the following XML element. It contains include and exclude XML elements
which are in no specific order. Even though they are not in any order the deserialization process is able to gather
the XML elements as thet are encountered.
<fileSet path="c:\">
<include pattern=".*.jar"/>
<exclude pattern=".*.bak"/>
<exclude pattern="~.*"/>
<include pattern=".*.class"/>
<exclude pattern="images/.*"/>
</fileSet>
In order to achieve this the following object can be used. This declares two inline collections which specify the name
of the entry objects that they are collecting. If the entry attribute is not specified then the name of the object will
be used instead.
@Root
public class FileSet {
@ElementList(entry="include", inline=true)
private List<Match> include;
@ElementList(entry="exclude", inline=true)
private List<Match> exclude;
@Attribute
private File path;
private List<File> files;
public FileSet() {
this.files = new ArrayList<File>();
}
@Commit
public void commit() {
scan(path);
}
private void scan(File path) {
File[] list = path.listFiles();
for(File file : list) {
if(file.isDirectory()) {
scan(path);
} else {
if(matches(file)) {
files.add(file);
}
}
}
}
private boolean matches(File file) {
for(Match match : exclude) {
if(match.matches(file)) {
return false;
}
}
for(Match match : include) {
if(match.matches(file)) {
return true;
}
}
return false;
}
public List<File> getFiles() {
return files;
}
@Root
private static class Match {
@Attribute
private String pattern;
public boolean matches(File file) {
Stirng path = file.getPath();
if(!file.isFile()) {
return false;
}
return path.matches(pattern);
}
}
}
Loose object mapping
An important feature for any XML tool is the ability to sift through the source XML to find particular
XML attributes an elements of interest. It would not be very convinient if you had to write an object
that accurately mapped every attribute an element in an XML document if all you are interested in is
perhaps an element and several attributes. Take the following XML document.
<contact id='71' version='1.0'>
<name>
<first>Niall</first>
<surname>Gallagher</surname>
</name>
<address>
<house>House 33</house>
<street>Sesame Street</street>
<city>City</city>
</address>
<phone>
<mobile>123456789</mobile>
<home>987654321</home>
</phone>
</example>
If my object only required the some of the details of the specified contact, for example the phone
contacts and the name then it needs to be able to ignore the address details safely. The following
code shows how this can be done by setting strict to false within the Root
annotation.
@Root(strict=false)
public class Contact {
@Element
private Name name;
@Element
private Phone phone;
public String getName() {
return name.first;
}
public String getSurname() {
return name.surname;
}
public String getMobilePhone() {
return phone.mobile;
}
public String getHomePhone() {
return phone.home;
}
private static class Name {
@Element
private String first;
@Element
private String surname;
}
private static class Phone {
@Element(required=false)
private String mobile;
@Element
private String home;
}
}
The above object can be used to parse the contact XML source. This simple ignores any XML elements or attributes that do not appear
in the class schema. To further clarify the implementation of loose mappings take the example shown below. This shows how the entry
object is deserialized from the above document, which is contained within a file. Once deserialized the object values can be examined.
Serializer serializer = new Persister();
File source = new File("contact.xml");
Contact contact = serializer.read(Contact.class, source);
assert contact.getName().equals("Niall");
assert contact.getSurname().equals("Gallagher");
assert contact.getMobilePhone().equals("123456789");
assert contact.getHomePhone().equals("987654321");
Java Bean serialization
Although field based serialization offers a simple and efficient means for serializing and deserializing
an object it can often be benificial to use Java Bean getters and setters to read and write values. In
particular annotating Java Bean setter and getter methods will allow for a cleaner means to override
the serialization behaviour than using fields. It also allows for processing and validation to be performed
as the object is being deserialized. Below is an example of how to annotate an objects methods for
use in the serialization process, this example mixes annotated fields with annotated methods.
@Root
public class Message {
private Collection<Entry> list;
@Attribute
private float version;
@ElementList
public void setList(Collection<Entry> entry) {
if(entry.isEmpty()) {
throw new IllegalArgumentException("Empty collection");
}
this.entry = entry;
}
@ElementList
public Collection<Entry> getList() {
return entry;
}
}
@Root
public class Entry {
@Attribute
public String name;
public String text;
@Text
public String getText() {
return text;
}
@Text
public void setText(String text){
this.text = text;
}
public String getName() {
return name;
}
}
In the above code the message class will have its methods invoked when a list of entry objects is
encountered. Here the method can perform some form of validation when the list of entry objects
is deserialized. Such validation can also be peformed using the persister callback methods, which is
described in a later section. The requirements for Java Bean method serialization are that
both the setter and getter must be annotated with the same annotation, and both annotations must
contain identical attributes. The object class schema could produce the following XML document.
<message version="1.2">
<list>
<entry name="a">Example text one</entry>
<entry name="b">Example text two</entry>
</list>
</message>
Example using template filters
Another very powerful feature with this XML serialization framework is the ability to use templating
when deserializing an XML document. This allows values within elements and attributes to use template variables
that can be replaced using a Filter object.
The simplest filter object is the map filter, which allows the user to place a Java map within the filter
object exposing the key value pairs to the templating system. The template system can now use the filter
to find replacement values for template variables within the XML document. To clarify take the following
example.
@Root
public class Layout {
@Element
private String path;
@Element
private String user;
@Attribute
private int id;
public String getPath() {
return path;
}
public String getUser() {
return user;
}
public int getId() {
return id;
}
}
The above object has declared two elements and an attribute to be deserialized from an XML document. These
values are typically static values within the XML source. However using a template variable syntax the
deserialization process will attempt to substitute the keys with values from the filter. Take the XML
document below with two template variables declared ${home.path} and ${user.name}.
<layout id="123">
<path>${home.path}</path>
<user>${user.name}</user>
</layout>
To ensure that these values can be replaced with user specified mappings a map filter can be used. Below
is an example of how to create a persister that can be given user specified key value pairs. Here the
above XML source is deserialized from a file and the annotated fields are given filter mappings if there
is a mapping specified.
Map map = new HashMap();
map.put("home.path", "/home/john.doe");
map.put("user.name", "john.doe");
Filter filter = new MapFilter(map);
Serializer serializer = new Persister(filter);
File source = new File("layout.xml");
Layout layout = serializer.read(Layout.class, source);
assert layout.getPath().equals("/home/john.doe");
assert layout.getUser().equals("john.doe");
As well as the map filter there are several stock filters which can be used to substitute template
variables with OS environment variables and JVM system properties. Also several template variables can
exist within the values. For example take the following XML document, which could be used in the
above example given that the mappings for ${first.name} and ${second.name} were added to the map filter.
<layout id="123">
<path>/home/${first.name}.${second.name}</path>
<user>${first.name}.${second.name}</user>
</layout>
Receiving persister callbacks
Of critical importance to the serialization and deserialization process is that the objects
have some control or participation in the process. It is no good to have the persister
deserialize the object tree from an XML document only to see that the data is not valid or
that further data structures need to be created in many of the deserialized objects.
To allow objects to participate in the deserialization process two annotations can be used,
these are the Validate and
Commit annotations.
Both are
involved in the deserialization process (not the serialization process) and are called
immediately after an object has been deserialized. Validation is performed first, and if the
deserialized object contains a method annotated with the validate annotation it is invoked.
This allows the object to perform validation of its fields, if the object requirements are
met the method returns quietly, if they are not met the object can throw an exception to
terminate the deserialization process. The commit method is invoked in much the same way,
the persister looks for a method marked with the commit annotation, if one exists it is
invoked. However, unlike the validate method the commit method is typically used to build
further data structures, for example hash tables or trees. Below is an example of an
object making use of these annotations.
@Root
public class PropertyMap {
private Map<String, Property> map;
@ElementList
private List<Property> list;
public PropertyMap() {
this.map = new HashMap<String, Entry>();
}
@Validate
public void validate() {
List<String> keys = new ArrayList<String>();
for(Property entry : list) {
String key = entry.getKey();
if(keys.contains(key)) {
throw new PersistenceException("Duplicate key %s", key);
}
keys.put(key);
}
}
@Commit
public void build() {
for(Property entry : list) {
insert(entry);
}
}
public void insert(Property entry) {
map.put(entry.getName(), entry);
}
public String getProperty(String name) {
return map.get(name).getValue();
}
}
The above object deserializes a list of property objects into a list. Once the property
objects have been deserialized they are validated by checking that an entry with a specific
key exists only once. After the validation process has completed the commit method is
invoked by the persister, here the object uses the deserialized property object to build
a hash table containing the property values keyed via the property key. Below is how
the above object would be represented as an XML document.
<properties>
<list>
<entry key="one">
<value>first value</value>
</entry>
<entry key="two">
<value>first value</value>
</entry>
<entry key="three">
<value>first value</value>
</entry>
</list>
</properties>
As well as annotations involved in the deserialization process there are annotations that can
be used to receive persister callbacks for the serialization process. Two annotations can
be used, they are the Persist
and Complete methods.
To receive persister callbacks the methods must be no argument methods marked with the
appropriate annotations.
The persist method is invoked before the serialization of the object. This allows the object
to prepare in some implementation specific way for the serialization process. This method
may throw an exception to terminate the serialization process. Once serialization has
completed the complete method is invoked. This allows the object to revert to its previous
state, that is, to undo what the persist method has done. Below is an example of how
these annotations can be used.
@Root
public class MailMessage {
@Attribute
private Stirng format;
@Element
private String encoded;
private byte[] content;
private Encoder encoder;
public MailMessage() {
this.encoder = new Encoder();
}
public void setEncoding(String format) {
this.format = format;
}
public String getEncoding() {
return format;
}
public void setMessage(byte[] content) {
this.content = content;
}
public byte[] getMessage() {
return content;
}
@Commit
public void commit() {
decoded = encoder.decode(encoded, format);
encoded = null;
}
@Persist
public void prepare() {
encoded = encoder.encode(decoded, format);
}
@Complete
public void release() {
encoded = null;
}
}
The above example illustrates how the persist and complete methods can be used in a
scenario where the serialization process needs to encode a byte array into a specific
encoding format. Before the object is persisted the persistable field is set to an
encoded string. When serialization has completed the encoded value is nulled to free
the memory it holds. This example is somewhat contrived however it effectively
demonstrates how the annotations can be used. Below is an example of what the XML
document should look like.
<mailMessage format="base64">
U2ltcGxlIGlzIGFuIFhNTCBzZXJpYWxpemF0aW9uIGZyYW1ld29yayBmb3IgSmF2YS4gSXRzIGdv
YWwgaXMgdG8gcHJvdmlkZSBhbiBYTUwgZnJhbWV3b3JrIHRoYXQgZW5hYmxlcyByYXBpZCBkZXZl
bG9wbWVudCBvZiBYTUwgY29uZmlndXJhdGlvbiBhbmQgY29tbXVuaWNhdGlvbiBzeXN0ZW1zLiBU
aGlzIGZyYW1ld29yayBhaWRzIHRoZSBkZXZlbG9wbWVudCBvZiBYTUwgc3lzdGVtcyB3aXRoIG1p
bmltYWwgZWZmb3J0IGFuZCByZWR1Y2VkIGVycm9ycy4gVGhlIGZyYW1ld29yayBib3Jyb3dzIGlk
ZWFzIGFuZCBjb25jZXB0cyBmcm9tIGV4aXN0aW5nIFhNTCB0b29scyBzdWNoIGFzIEMjIFhNTCBz
ZXJpYWxpemF0aW9uIGFuZCBvdGhlciBwcm9wcmlldGFyeSBmcmFtZXdvcmtzIGFuZCBjb21iaW5l
cyB0aG9zZSBpZGVhcyByZXN1bHRpbmcgaW4gYSBzaW1wbGUgeWV0IGV4dHJlbWVseSBwb3dlcmZ1
bCB0b29sIGZvciB1c2luZyBhbmQgbWFuaXB1bGF0aW5nIFhNTC4gQmVsb3cgaXMgYSBsaXN0IG9m
IHNvbWUgb2YgdGhlIGNhcGFiaWxpdGllcyBvZiB0aGUgZnJhbWV3b3JrLiA=
</mailMessage>
For the above XML message the contents can be serialized and deserialized safely
using persister callbacks. The object can prepare itself before serialization
by encoding the contents of the message to the encoding format specified. Once it has
been encoded and serialized any resources created for serialization can be released.
Maintaining state between persister callbacks
When serializing and deserializing objects there is often a need to share information between
callbacks without affecting the object implementation. In order to achieve this the persister
can provide a session map to the methods annotated for persister callbacks. Below is an example
of a serializable object that can receive a persister session object.
@Root
public class Person {
@ElementList
private List<Variable> details;
@Element
private Address address;
private List names;
@Validate
public void validate(Map session) throws PersistenceException {
if(session.isEmpty()) {
throw new PersistenceException("Map must not be empty")
}
}
@Commit
public void commit(Map session) {
Set keys = session.keySet();
for(Object item : keys) {
names.add(item);
}
}
}
@Address
public class Address {
@Element
private String street;
@Element
private String city;
@Element
private String state;
public String getStreet() {
return street;
}
public String getCity() {
return city;
}
public String getState() {
return state;
}
}
@Root
public class Variable {
@Attribute
private String name;
@Attribute
private String value;
@Commit
public void commit(Map session) {
session.put(name, value);
}
public String getName() {
return name;
}
public String getValue() {
return value;
}
}
The above example shows how entry objects can pass there names to its parent during the
deserialization process. To clarify, deserialization is performed in a depth first manner
so for this example the entry objects will be initialized and have their callback methods
invoked before the root example class.
Although this may not seem like a very powerful feature, it offers great capabilities
when paired with the templating system described earlier. The templating engine has
access to all details placed into the session map object. So other values within the XML
document can reference each other. For example take the XML document below for the above
objects.
<person>
<details>
<var name="name" value="John Doe"/>
<var name="street" value="Sesame Street"/>
<var name="city" value="Metropolis"/>
<var name="state" value="Some State"/>
</details>
<address>
<street>${street}</street>
<city>${city}</city>
<state>${state}</state>
</address>
</person>
The above XML document illustrates how the variable objects values are accessible to the
elements declared in the address element. The street, city, and state needed to be
defined only once to be shared throughout the document.
Serializing with CDATA blocks
At times it is nessecary to serialize large text and element data values. Such values may
also contain formatting that you wish to preserve. In such situations it is often best to
wrap the values within XML CDATA blocks. The CDATA block can contain XML characters and
formatting information which will not be modified by other XML parsers. For example take
the following XML source.
<query type="scrape" name="title">
<data><![CDATA[
<news>
{
for $text in .//B
return $text
}
</news>
]]></data>
</query>
The above XML there is an embedded XQuery expression which is encapsulated within a CDATA
block. Such a configuration allows the XQuery expression to exist within the XML document
without any need to escape the XML characters. Also, if the XQuery expression was very
large then this form of encoding would provide better performance. In order to ensure that
the data is maintained within the CDATA block the following could be used.
@Root
public class Query {
@Attribute
private String scrape;
@Attribute
private String title;
@Element(data=true)
private String data;
public String getData() {
return data;
}
public String getTitle() {
return title;
}
public String getScrape() {
return scrape;
}
}
Here the Element annotation
has the data attribute set to true. This tells the serialization process that any value stored
within the data field must be written to the resulting XML document within a CDATA block. The
data attribute can be used with the Text,
ElementArray, and ElementList annotations also.
Resolving object reference cycles
When there are cycles in your object graph this can lead to recursive serialization. However it
is possible to resolve these references using a stock strategy. The
CycleStrategy maintains
the object graph during serialization and deserialization such that cyclical references can be traced
and resolved. For example take the following object relationships.
@Root
public class Parent {
private Collection<Child> children;
private String name;
@Attribute
public String getName() {
return name;
}
@Attribute
public void setName(String name) {
this.name = name;
}
@Element
public void setChildren(Collection<Child> children) {
this.children = children;
}
@Element
public Collection<Child> getChildren() {
return children;
}
public void addChild(Child child) {
children.add(child);
}
}
@Root
public class Child {
private Parent parent;
private String name;
public Child() {
super();
}
public Child(Parent parent) {
this.parent = parent;
}
@Attribute
public String getName() {
return name;
}
@Attribute
public void setName(String name) {
this.name = name;
}
@Element
public Parent getParent() {
return parent;
}
@Element
public void setParent(Parent parent) {
this.parent = parent;
}
}
In the above code snippet the cyclic relation ship between the parent and child can be seen. A parent can have
multiple children and a child can have a reference to its parent. This can cause problems for some XML binding
and serialization frameworks. However this form of object relationship can be handled seamlessly using the
CycleStrategy object. Below is an
example of what a resulting XML document might look like.
<parent name="john" id="1">
<children>
<child id="2" name="tom">
<parent ref="1"/>
</child>
<child id="3" name="dick">
<parent ref="1"/>
</child>
<child id="4" name="harry">
<parent ref="1"/>
</child>
</children>
</parent>
As can be seen there are two extra attributes present, the id attribute and the ref attribute. These references
are inserted into the serialized XML document when the object is persisted. They allow object relationships and
references to be recreated during deserialization. To further clarify take the following code snippet which shows
how to create a persister that can handle such references.
Strategy strategy = new CyclicStrategy("id", "ref");
Serializer serializer = new Persister(strategy);
File source = new File("example.xml");
Parent parent = serializer.read(Parent.class, source);
The strategy is created by specifying the identity attribute as id and the refering attribute as ref. For convinience
these attributes have reasonable defaults and the no argument constructor can be used to create the strategy. Although
the example shown here is very simple the cycle strategy is capable of serializing and deserializing large and complex
relationships.
Reusing XML elements
As can be seen from using the CycleStrategy
in the previous section object references can easily be maintained regardless of complexity. Another benifit of using the
cycle strategy is that you can conviniently reuse elements when creating configuration. For example take the following
example of a task framework.
@Root
public class Workspace {
@Attribute
private File path;
@Attribute
private String name
private File getPath() {
return path;
}
private String getName() {
return name;
}
}
@Root
public abstract Task {
@Element
private Workspace workspace;
public abstract void execute() throws Exception;
}
public class DeleteTask extends Task {
@ElementList(inline=true, entry="resource")
private Collection<String> list;
public void execute() {
File root = getPath();
for(String path : list) {
new File(root, path).delete();
}
}
}
public class MoveTask extends Task {
@ElementList(inline=true, entry="resource")
private Collection<String> list;
@Attribute
private File from;
public void execute() {
File root = getPath();
for(String path : list) {
File create = new File(root, path);
File copy = new File(from, path);
copy.renameTo(create);
}
}
}
The above code snippet shows a very simple task framework that is used to perform actions on a workspace.
Each task must contain details for the workspace it will perform its specific task on. So, making use of
the cycle strategy it is possible to declare a specific object once, using a know identifier and referencing
that object throughout a single XML document. This eases the configuration burden and ensures that less errors
can creap in to large complex documents where may objects are declared.
<job>
<workspace id="default">
<path>c:\workspace\task</path>
</workspace>
<task class="example.DeleteTask">
<workspace ref="default"/>
<resource>output.dat</resource>
<resource>result.log</resource>
</task>
<task class="example.MoveTask">
<workspace ref="default"/>
<from>c:\workspace\data</from>
<resource>input.xml</resource>
</task>
</job>
Using utility collections
For convinience there are several convinience collections which can be used. These collections only need to be
annotated with the ElementList annotation
to be used. The first stock collection resembles a map in that it will accept values that have a known
key or name object, it is the Dictionary collection.
This collection requires objects of type Entry to
be inserted on deserialization as this object contains a known key value. To illustrate how to use this collection take the
following example.
@Root
public class TextMap {
@ElementList(inline=true)
private Dictionary<Text> list;
public Text get(String name) {
return list.get(name);
}
}
@Root
public class Text extends Entry {
@Text
public String text;
public String getText() {
return text;
}
}
The above objects show how the dictionary collection is annotated with the element list annotation. The containing
object can not serialize and deserialize entry objects which can be retrieve by name. For example take the
following XML which shows the serialized representation of the text map object.
<textMap>
<text name="name">Niall Gallagher</text>
<text name="street">Seasme Street</text>
<text name="city">Atlantis</text>
</textMap>
Each text entry deserialized in to the dictionary can now be acquired by name. Although this offers a convinient
map like structure of acquring objects based on a name there is often a need to match objects. For such a requirement
the Resolver collection can be used. This
offers a fast pattern matching collection that matches names or keys to patterns. Patterns are deserialized within
Match objects, which are inserted in to the
resolver on deserialization. An example of the resolver is shown below.
@Root
private static class ContentType extends Match {
@Attribute
private String value;
public ContentType() {
super();
}
public ContentType(String pattern, String value) {
this.pattern = pattern;
this.value = value;
}
}
@Root
private static class ContentResolver implements Iterable {
@ElementList
private Resolver<ContentType> list;
@Attribute
private String name;
public Iterator<ContentType> iterator() {
return list.iterator();
}
public ContentType resolve(String name) {
return list.resolve(name);
}
}
The above content resolver will match a string with a content type. Such an arrangement could be used to resolve
paths to content types. For example the following XML document illustrates how the resolver could be used
to match URL paths to content types for a web application.
<contentResolver name='example'>
<contentType pattern='*.html' value='text/html'/>
<contentType pattern='*.jpg' value='image/jpeg'/>
<contentType pattern='/images/*' value='image/jpeg'/>
<contentType pattern='/log/**' value='text/plain'/>
<contentType pattern='*.exe' value='application/octetstream'/>
<contentType pattern='**.txt' value='text/plain'/>
<contentType pattern='/html/*' value='text/html'/>
</contentResolver>
Although the resolver collection can only deal with wild card characters such as * and ? it is much faster than
resolutions performed using Java regular expressions. Typically it is several orders of magnitude faster that
regular expressions, particularly when it is used to match reoccuring values, such as URI paths.
Object substitution
Often there is a need to substitute an object into the XML stream either during serialization or deserialization. For example
it may be more convinient to use several XML documents to represent a configuration that can be deserialized in to a single
object graph transparently. For example take the following XML.
<registry>
<import name="external.xml" class="example.ExternalDefinition"/>
<define name="blah" class="example.DefaultDefinition">
<property key="a">Some value</property>
<property key="b">Some other value</property>
</define>
</registry>
In the above XML document there is an import XML element, which references a file external.xml. Given that this
external file contains further definitions it would be nice to be able to replace the import with the definition
from the file. In such cases the Resolve
annotation can be used. Below is an example of how to annotate your class to substitute the objects.
@Root
private class Registry {
@ElementList(inline=true)
private Dictionary<Definition> import;
@ElementList(inline=true)
private Dictionary<Definition> define;
public Definition getDefinition(String name) {
Definition value = define.get(name);
if(value == null) {
value = import.get(name);
}
return value;
}
}
public interface Definition {
public String getProperty(String key);
}
@Root(name="define")
public class DefaultDefinition implements Definition {
@ElementList(inline=true)
private Dictionary<Property> list;
public String getProperty(String key) {
return list.get(key);
}
}
@Root(name="import")
public class ExternalDefinition implements Definition {
@Element
private File name;
public String getProperty(String key) {
throw new IllegalStateException("Method not supported");
}
@Resolve
public Definition substitute() throws Exception {
return new Persister().read(Definition.class, name);
}
}
Using this form of substitution objects can be replaced in such a way that
deserialized objects can be used as factories for other object instances. This
is similar to the Java serialization concept of readResolve and writeReplace methods.
Serializing Java language types
A common requirement of any serialization framework is to be able to serialize and deserialize existing types without modification. In particular types from the Java class libraries, like dates, locales, and files. For many of the Java class library types there is a corrosponding Transform implementation, which enables the serialization and deserialization of that type. For example the java.util.Date type has a transform that accepts a date instance and transforms that into a string, which can be embedded in to the generated XML document during serialization. For deserialization the same transform is used, however this time it converts the string value back in to a date instance. The code snippet below demonstrates how a such transformations make it possible to use such a type when implementing your class XML schema.
@Root
public class DateList {
@Attribute
private Date created;
@ElementList
private List<Date> list;
public Date getCreationDate() {
retrun created;
}
public List<Date> getDates() {
return list;
}
}
Here the date object is used like any other Java primitive, it can be used with any of the XML annotations. Such objects can also be used with the CycleStrategy so that references to a single instance within your object graph can be maintained throughout serialization and deserialization operations. Below is an example of the XML document generated.
<dateList created="2007-01-03 18:05:11.234 GMT">
<list>
<date>2007-01-03 18:05:11.234 GMT</date>
<date>2007-01-03 18:05:11.234 GMT</date>
</list>
</dateList>
Using standard Java types, such as the Date type, can be used with any of the XML annotations. The set of supported types is shown below. Of particular note are the primitive array types, which when used with the ElementArray annotation enable support for multidimentional arrays.
char
char[]
java.lang.Character
java.lang.Character[]
int
int[]
java.lang.Integer
java.lang.Integer[]
short
short[]
java.lang.Short
java.lang.Short[]
long
long[]
java.lang.Long
java.lang.Long[]
double
double[]
java.lang.Double
java.lang.Double[]
byte
byte[]
java.lang.Byte
java.lang.Byte[]
float
float[]
java.lang.Float
java.lang.Float[]
boolean
boolean[]
java.lang.Boolean
java.lang.Boolean[]
java.lang.String
java.lang.String[]
java.util.Date
java.util.Locale
java.util.Currency
java.util.TimeZone
java.util.GregorianCalendar
java.net.URL
java.io.File
java.math.BigInteger
java.math.BigDecimal
java.sql.Date
java.sql.Time
java.sql.Timestamp
For example take the following code snippet, here points on a graph are represented as a multidimentional array of integers. The array is annotated in such a way that it can be serialized and deserialized seamlessly. Each index of the array holds an array of type int, which is transformed using the Transformer in to a comma separated list of integer values. Obviously this is not of much use in a real world situation, however it does illustrate how the transformable types can be integrated seamlessly with existing XML annotations.
@Root
public class Graph {
@ElementArray(entry="point")
private int[][] points;
public Graph() {
super();
}
@Validate
private void validate() throws Exception {
for(int[] array : points) {
if(array.length != 2) {
throw new InvalidPointException("Point can not have %s values", array.length);
}
}
}
public int[][] getPoints() {
return points;
}
}
For the above code example the resulting XML generated would look like the XML document below. Here each index of the element array represents an array of integers within the comma separated list. Such structures also work well with the cycle strategy in maintaining references.
<graph>
<points length='4'>
<point>3, 5</point>
<point>5, 6</point>
<point>5, 1</point>
<point>3, 2</point>
</points>
</graph>
Styling serialized XML
In order to serialize objects in a consistent format a
Style implementation
can be used to format the elements and attributes written to the XML document. Styling of XML allows
both serialization and deserialization to be performed. So once serialized in a styled XML format you can
deserialize the same document back in to an object.
@Root
public class PersonProfile {
@Attribute
private String firstName;
@Attribute
private String lastName;
@Element
private PersonAddress personAddress;
@Element
private Date personDOB;
public Date getDateOfBirth() {
return personDOB;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public PersonAddress getAddress() {
return personAddress;
}
}
@Root
public class PersonAddress {
@Element
private String houseNumber;
@Element
private String streetName;
@Element
private String city;
public String getHouseNumber() {
return houseNumber;
}
public String getStreetName() {
return streetName;
}
public String getCity() {
return city;
}
}
For example, taking the above annotated objects. An instance of the person profile can be serialized in
to an XML document that is styled with a hyphenated format. This produces a consistently formated
result which is just as deserializable as a serialization that is not styled.
<person-profile first-name='Niall' last-name='Gallagher'>
<person-DOB>10/10/2008</person-DOB>
<person-address>
<house-number>10</house-number>
<street-name>Sesame Street</street-name>
<city>Disney Land</city>
</person-address>
</person-profile>
In order to serialize an object in a styled format either the
HyphenStyle or
CamelCaseStyle can
be used. If neither suits one can always be implemented. Also, for convenience any of the elements or
attributes can be overridden with a specific string by setting it to the style instance. The code snippet
below shows how to serialize the object in the hyphenated style above.
Style style = new HyphenStyle();
Format format = new Format(style);
Serializer serializer = new Persister(format);
serializer.write(personDetail, file);
Version tolerant serialization
In order to serialize objects in a version tolerant format a
Version annotation can
be introduced to the class. This will allow a later, modified class to be read from XML generated by
the original class. For example take the following code snippet showing an annotated class.
@Root
@Namespace(prefix="p", reference="http://www.domain.com/person")
public class Person {
@Attribute
private String name;
@Element
private String height;
@Element
private String weight;
public String getName() {
return name;
}
public String getHeight() {
return height;
}
public String getWeight() {
return weight;
}
}
The above annotated class schema will generate XML in a format compatible with that class. For example, a serialization of the class could result in the following XML snippet. This shows the height and weight elements as well as the name attribute.
<p:person name'John Doe' xmlns:p='http://www.domain.com/person'>
<p:height>185</p:height>
<p:weight>84</p:height>
</p:person>
Having used this class schema to serialize instances of the Person class, It could later be extended or modified as follows and still read and write in a format compatible with the old class schema like so, even though the resulting XML has changed.
@Root
@Namespace(prefix="p", reference="http://www.domain.com/person")
public class Person {
@Version(revision=1.1)
private double version;
@Attribute
private String name;
@Attribute
private int age;
@Element
private int height;
@Validate
private void validate() {
// provide defaults here ...
}
public String getName() {
return name;
}
public int getHeight() {
return height;
}
public int getAge() {
return age;
}
}
Here the version attribute is annotated with the special Version annotation. This will read the previously generated XML and compare the version attribute of the person element and compare it to the revision attribute of the annotation. If the version annotation does not exist the initial 2.0 version is assumed. So when using the new modified class, which is revision 1.1, with the old serialized XML the serializer will determine that the two have differing versions. So when deserializing it will ignore the excess weight element and ignore the fact that the age attribute does not exist. It will do this for all attributes and elements that do not match.
This is quite similar to the C# XML serialization version capability. Where the initial version of each class is 1.0 (implicitly) and subsequent versions increase. This tells the serializer how it should approach deserialization of different versions. The later version of the class when serialized will explicitly write the version as follows.
<p:person version='1.1' name'John Doe' age='60' xmlns:p='http://www.domain.com/person'>
<p:height>185</p:height>
</p:person>
Serializing static final fields
Often there is a need to add elements and attributes to an XML document that does not change. In such
an event it is often attractive to declare these fields as static final fields. When annotating
static final fields they form part of the XML schema and contribute to the validation of the document
but do not get set when deserializing the XML in to a object instance. So should a required static
final field not exist in the source XML then an exception is thrown when deserializing, much like what
would happen if the field was mutable. For example take the code snippet below, which shows static
final primitive fields and a static final composite object.
@Root
@Namespace(reference = "http://www.domain.com/document")
public class Document {
@Element(name="author")
@Namespace(prefix="user", reference="http://www.domain.com/user")
private static final String AUTHOR = "Niall Gallagher";
@Element(name="contact")
private static final String CONTACT = "niallg@users.sourceforge.net";
@Element(name="detail")
private static final Detail DETAIL = new Detail(
"Stanford Press",
"2001",
"Palo Alto",
"1st",
"0-69-697269-4");
@ElementList(inline=true)
private List<Section> list;
@Attribute
private String title;
private Document() {
super();
}
public Document(String title) {
this.list = new ArrayList<Section>()
this.title = title;
}
public void add(Section section) {
list.add(section);
}
}
@Root
@Namespace(reference="http://www.domain.com/detail")
public class Detail {
@Element
private String publisher;
@Element
private String date;
@Element
private String address;
@Element
private String edition;
@Element
private String ISBN;
private Detail() {
super();
}
public Detail(String publisher, String date, String address, String edition, String ISBN) {
this.publisher = publisher;
this.address = address;
this.edition = edition;
this.date = date;
this.ISBN = ISBN;
}
}
@Root
@NamespaceList({
@Namespace(prefix="para", reference="http://www.domain.com/paragraph")
})
public class Section {
@Attribute
private String name;
@ElementList(inline = true)
private List<Paragraph> list;
private Section() {
super();
}
public Section(String name) {
this.list = new ArrayList<Paragraph>();
this.name = name;
}
public void add(Paragraph paragraph) {
list.add(paragraph);
}
}
@Root
@Namespace(reference = "http://www.domain.com/paragraph")
public class Paragraph {
private String text;
@Text
private String getContent() {
return text;
}
@Text
public void setContent(String text) {
this.text = text;
}
}
The above annotated objects form an object model to describe a simple document structure. The document object itself contains static final fields that
will be written to the resulting XML document when serialized. However, on deserialization the values read from the XML will not change the annotated
fields. Instead the deserialization process will simply validate the presence of the elements and attributes within the document. This results in
an object that will always write the same values for specific elements and attributes.
<document title="Secret Document" xmlns="http://www.domain.com/document">
<user:author xmlns:user="http://www.domain.com/user">Niall Gallagher</user:author>
<contact>niallg@users.sourceforge.net</contact>
<detail xmlns="http://www.domain.com/detail">
<publisher>Stanford Press</publisher>
<date>2001</date>
<address>Palo Alto</address>
<edition>1st</edition>
<ISBN>0-69-697269-4</ISBN>
</detail>
<section name="Introduction" xmlns:para="http://www.domain.com/paragraph">
<para:paragraph>First paragraph of document</para:paragraph>
<para:paragraph>Second paragraph in the document</para:paragraph>
<para:paragraph>Third and final paragraph</para:paragraph>
</section>
</document>
|