simple2.0使用java的xml序列化

来源:互联网 发布:科比1213赛季数据 编辑:程序博客网 时间:2024/05/11 22:44

http://simple.sourceforge.net/download/stream/doc/tutorial/tutorial.php

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.

  1. Serializing a simple object
  2. Deserializing a simple object
  3. Nested object serialization
  4. Optional elements and attributes
  5. Reading a list of elements
  6. Overriding an annotated type
  7. Dealing with an inline list of elements
  8. Reading an array of elements
  9. Adding text and attributes to elements
  10. Dealing with map objects
  11. Scattering inline element entries
  12. Loose object mapping
  13. Java Bean serialization
  14. Example using template filters
  15. Receiving persister callbacks
  16. Maintaining state between persister callbacks
  17. Serializing with CDATA blocks
  18. Resolving object reference cycles
  19. Reusing XML elements
  20. Using utility collections
  21. Object substitution
  22. Serializing Java language types
  23. Styling serialized XML
  24. Version tolerant serialization

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.

@Rootpublic 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.

@Rootpublic 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.

@Rootpublic 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.

@Rootpublic class PropertyList {   @ElementList   private List<Entry> list;   @Attribute   private String name;   public String getName() {      return name;   }   public List getProperties() {      return list;   }}@Rootpublic 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.

@Rootpublic 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();}@Rootpublic 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.

@Rootpublic 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;@Rootpublic 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;   }}@Rootpublic 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.

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.

@Rootpublic 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;              }}@Rootpublic 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.

@Rootpublic 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.

@Rootpublic 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.

@Rootpublic 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.

@Rootpublic 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;              }}@Rootpublic 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.

@Rootpublic 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.

@Rootpublic 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.

@Rootpublic 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.

@Rootpublic 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);      }   }}@Addresspublic 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;   }}@Rootpublic 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.

@Rootpublic 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.

@Rootpublic 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);              }}@Rootpublic 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.

@Rootpublic class Workspace {   @Attribute   private File path;   @Attribute   private String name   private File getPath() {      return path;              }   private String getName() {      return name;              }}@Rootpublic 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.

@Rootpublic class TextMap {      @ElementList(inline=true)   private Dictionary<Text> list;      public Text get(String name) {      return list.get(name);              }}@Rootpublic 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.

@Rootprivate static class ContentType extends Match {   @Attribute   private String value;           public ContentType() {      super();                     }   public ContentType(String pattern, String value) {      this.pattern = pattern;      this.value = value;           }}   @Rootprivate 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.

@Rootprivate 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.

@Rootpublic 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>        <object>2007-01-03 18:05:11.234 GMT</date>        <object>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.
charchar[]java.lang.Characterjava.lang.Character[]intint[]java.lang.Integer java.lang.Integer[]shortshort[]java.lang.Shortjava.lang.Short[]long        long[]java.lang.Longjava.lang.Long[]doubledouble[]java.lang.Doublejava.lang.Double[]bytebyte[]java.lang.Bytejava.lang.Byte[]float       float[]        java.lang.Float        java.lang.Float[]        booleanboolean[]java.lang.Booleanjava.lang.Boolean[]java.lang.Stringjava.lang.String[]java.util.Datejava.util.Localejava.util.Currencyjava.util.TimeZonejava.util.GregorianCalendarjava.net.URLjava.io.Filejava.math.BigIntegerjava.math.BigDecimaljava.sql.Datejava.sql.Timejava.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.

@Rootpublic 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.

@Rootpublic 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;   }}@Rootpublic 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>
原创粉丝点击