Basic Persistence using XStream

Basic Persistence using XStream

By Mark Halloran, OCI Senior Software Engineer

November 2009


Introduction

Many tasks in software engineering provide unique opportunities to fail. Persistence is a very good example, because we, the application engineers, have to live with the decisions we make for an extended period of time, and decisions made based on initial information might be the incorrect decisions when more information is known.

XStream is a simple library to serialize objects to XML and back again. Using it correctly to stream persistent class data to XML provides a sound and stable base from which persistence can be accomplished.

In this article, I demonstrate some basic tenets to be considered when designing persistence using XStream for an application.

Along with this discussion, I am providing code fragments to illustrate particular points.

XStream can be obtained from: http://xstream.codehaus.org/, and it is available under a BSD license. You will also require the open-source XPP3 (XML Pull Parser), which can be obtained from: http://www.extreme.indiana.edu/dist/java-repository/xpp3/distributions/

I am using Java2D objects to represent a portion of the application's state.

Establish a dichotomy between an application's model and behavior

Dichotomy: (Webster's Dictionary)
     the division into two especially mutually exclusive or contradictory groups or entities ,
     also: the process or practice of making such a division

The object-oriented concept of encapsulation provides benefits of co-locating attributes and behavior. Persistence requires we separate them, allowing a component of an application to be saved and subsequently restored to a usable state.

Frequently, establishing the correct dichotomy between state and behavior can be a source of problems. It serves one well to pay careful attention to establishing a sound and consistent separation.

In our example, we have defined an abstract class GeneralArea from which we extend a GeneralRect and GeneralCirc. These areas can be added to an assembly (which we'll introduce later):

  1. public abstract class GeneralArea {
  2. private static final String GENERAL_AREA_VERSION = "generalAreaVersion";
  3.  
  4. protected static final double DEFAULT_ORIGIN_X = 0.0;
  5. protected static final double DEFAULT_ORIGIN_Y = 0.0;
  6. public static final double AREA_EPSILON = 1e-12;
  7.  
  8. private double originX = DEFAULT_ORIGIN_X;
  9. private double originY = DEFAULT_ORIGIN_Y;
  10. private int generalAreaVersion = 1;
  11. [...]
  12. }
  13.  
  14. public abstract class GeneralRect extends GeneralArea {
  15. private static final double DEFAULT_WIDTH = 2.0;
  16. private static final double DEFAULT_HEIGHT = 1.0;
  17.  
  18. // persisted data.
  19. public double width = DEFAULT_WIDTH;
  20. public double height = DEFAULT_HEIGHT;
  21. [...]
  22. }
  23.  
  24. public abstract class GeneralCirc extends GeneralArea {
  25. private static final double DEFAULT_DIAMETER = 1.0;
  26.  
  27. // persisted data.
  28. public double diameter = DEFAULT_DIAMETER;
  29. [...]
  30. }

We certainly will need to maintain attributes such as the origin of any area, as well as the width and height of a rectangle and the diameter of the circle. These are attributes required to define the shape of the area. But, if we wish also draw these areas using Java2D, we'll need to use an object of type java.awt.geom.Area. We'll add this to our GeneralArea class, and add an attribute to hold on to the java.awt.geom.Area attribute as well.

If you consider the application's lifecycle, you will notice that the area attribute can always be re-generated from the input values of origin and size. As we extend our dynamic model to maintain the created area from our general areas, we will choose to not persist the generated Java2D Area.

  1. import java.awt.geom.Area;
  2.  
  3. public abstract class GeneralArea {
  4. private static final String GENERAL_AREA_AREA = "area";
  5. private transient Area area;
  6.  
  7. protected static final double DEFAULT_ORIGIN_X = 0.0;
  8. protected static final double DEFAULT_ORIGIN_Y = 0.0;
  9. public static final double AREA_EPSILON = 1e-12;
  10.  
  11. private double originX = DEFAULT_ORIGIN_X;
  12. private double originY = DEFAULT_ORIGIN_Y;
  13. [...]
  14. }

Notice that the area attribute has been declared as transient. This is one way of indicating to XStream that serialization (persistence for us) is not required.

A second approach, more explicit, is to tell XStream directly that a particular attribute (or field) should be omitted.

I prefer utilizing both approaches to ensure follow-on developers do not miss intended targets of persistence. This approach is:

   XStream:omitField(Class type, String fieldName);

I provide a method ...

public static void setupXStream(XStream xstream);

... in each of my persistent classes to provide for customization of XStream.

The class method in GeneralArea is such:

  1. public static void setupXStream(XStream xstream) {
  2. [...]
  3. xstream.omitField(GeneralArea.class, GENERAL_AREA_AREA);
  4. [...]
  5.  
  6. GeneralRect.setupXStream(xstream);
  7. GeneralCirc.setupXStream(xstream);
  8. }

Notice that I also force the setup for derived classes, a simplification I hope you'll excuse for brevity's sake.

We now can distinguish and indicate to XStream the attributes we wish to persist and those we wish to ignore. By doing this, we have limited our exposure from persisting classes over which we do not have direct control. Consider this when choosing how to maintain state, especially when GUI classes are involved; these can become a detriment as an application matures.

Protect against refactoring during ongoing development

As we evolve our designs, we often move classes from one package into another. By default, XStream uses a fully qualified name to resolve class names for persisted objects. This removes ambiguity from persisted classes in different packages that have the same name. However, it also prohibits a class from being recognized when refactored into another package.

To alleviate this scenario, XStream provides the following convenience method that allows a class to be recognized after a move.

   xstream.alias(String nameToAlias, Class classToAlias);

Exercising care in naming is very important to disambiguate persisted objects. Remember to consider ancillary classes that may be included in your class to make sure you've accounted for each of the included classes.

Incorporate Extensibility to Persisted Objects

An application maintaining persistent data will undergo changes as requirements are added, changed, or re-implemented. Providing versioning for each of these objects allows new implementations to accept and modify legacy persisted objects when previous versions of data are restored.

Adding requirements

I will introduce the concept of an assembly. This serves to collect GeneralAreas. Further, we want to be able to add and subtract areas from this assembly; for instance, we can now add a positive rectangle and then subtract a negative circle, yielding a hole in the rectangle.

Providing the assembly

We'll make the GeneralArea responsible for maintaining its negative state. The assembly is responsible only for maintaining the [ordered] collection of GeneralAreas. Here's our new class:

  1. public class Assembly {
  2.  
  3. // Tags used to facilitate XStream.
  4. //
  5. private static final String XSTREAM_ID = "Assembly";
  6. private static final String ASSEMBLY_VERSION = "assemblyVersion";
  7. private static final String GENERAL_AREAS = "generalAreas";
  8. private static final String CHANGE_SUPPORT = "changeSupport";
  9. private static final String XSTREAM = "xstream";
  10.  
  11. // NON-PERSISTED data, must be regenerated on read or access.
  12. //
  13. private transient XStream xstream;
  14. private transient Area area = null;
  15.  
  16. // Persisted attributes.
  17. //
  18. protected List<GeneralArea> generalAreas = new ArrayList<GeneralArea>();
  19. protected int assemblyVersion = 1;
  20.  
  21. public Assembly() {
  22. initializeXStream();
  23. }
  24.  
  25. public void add(GeneralArea generalArea) {
  26. area = null;
  27. generalAreas.add(generalArea);
  28. }
  29.  
  30. public Area getArea() {
  31. if (area == null) {
  32. area = new Area();
  33. for (GeneralArea generalArea : generalAreas) {
  34. if (generalArea.isNegative()) {
  35. area.subtract(generalArea.getArea());
  36. } else {
  37. area.add(generalArea.getArea());
  38. }
  39. }
  40. }
  41. return area;
  42. }
  43. [...]
  44. private void initializeXStream() {
  45. if (xstream == null) {
  46. xstream = new XStream(new XppDomDriver());
  47. setupXStream(xstream);
  48. }
  49. }
  50.  
  51. public static void setupXStream(XStream xstream) {
  52. xstream.alias(XSTREAM_ID, Assembly.class);
  53. xstream.useAttributeFor(Assembly.class, ASSEMBLY_VERSION);
  54. xstream.addImplicitCollection(GeneralArea.class, GENERAL_AREAS);
  55.  
  56. xstream.omitField(GeneralArea.class, XSTREAM);
  57.  
  58. GeneralArea.setupXStream(xstream);
  59. }
  60.  
  61. public String saveToXStream() {
  62. initializeXStream();
  63. return xstream.toXML(this);
  64. }
  65. }

Notice that the area attribute is transient, and I use lazy evaluation to acquire a valid summation of the collected generalAreas. I do this to simplify the difference between a constructed model, a read-in model, or (as you can see in the add method) a modified model.

I'm making the assumption that at any or all times, the overall area may not have been generated. This provides consistent access to the model throughout its lifetime.

Modifying the GeneralArea

Next, we add a boolean isNegative attribute to GeneralArea. Since we have maintained a version attribute that is streamed in and out, now we will bump the version and introduce a new method:

private Object readResolve();

readResolve provides access (as does the JDK serialization) to the instantiated object of a class after it is populated. This provides a place to perform default construction and version maintenance on data. We will use it to perform version maintenance:

private Object readResolve() {
    if (generalAreaVersion < GENERAL_AREA_VERSION_2) {
        isNegative = false;
        generalAreaVersion = GENERAL_AREA_VERSION_2;
    }
    return this;
}
 

Writing and Reading the Persisted Data

XStream provides a method for outputting the data of your classes as XML. Note however, that XStream does not generate the XML headers – only consistent chunks of your data that can be embedded in a complete HTML document.

I do not pollute the object model with the responsibility of providing header data; I allow the user of the XML output to construct the document using the XML output string data describing my persistent structures.

As the assembly is the highest level of this small architecture, I place the responsibility of encoding our classes at the Assembly level by implementing:

public String toXMLString() {
    initializeXStream();
    return xstream.toXML(this);
}

After consideration, preparation, and implementation, it becomes that simple to extract your data model to XML.

Consider the application lifecycle when determining what to persist

Let's review some of the decisions I have made during development of this small set of code.

I chose not to persist data that is completely dependent upon more basic data. This simplifies assumptions regarding consistency. What happens if I save a modified but not updated model, when an origin has been edited, but the resulting Java2D Area has not been regenerated? By omitting values I can generate, I remove the issue.

There are also attributes in the included source code required to provide behavior necessary for a working application, like PropertyChange. This attribute contributes nothing to the model, only run-time behavior. Thus, I do not persist it. Rather, I use readResolve to re-hookup listeners for a streamed-in model.

Summary

I hope that this has provided a basic front-to-back understanding of utilizing XStream to provide persistence.

Please utilize the source code to further explore XStream usage.

References



Software Engineering Tech Trends (SETT) is a regular publication featuring emerging trends in software engineering.