Sun Java Solaris Communities My SDN Account Join SDN
 
Article

Using XMLEncoder

 

Using XMLEncoder

by Philip Milne

This article covers advanced use of XMLEncoder, showing how it can be configured to create archives of any Java objects -- even when they don't follow the JavaBeans conventions. We include examples of how to make properties "transient" and how to create archives that call constructors with arguments, use static factory methods, and perform non-standard initialization steps. We also cover exception notification, the "owner" property (which can be used to a link an archive to the outside world), and methods for creating internationalized archives.

Introduction to XMLEncoder

The XMLEncoder class can be used to create an XML document that describes the state of a JavaBeans component (a bean), much like the way the ObjectOutputStream class can be used to create binary files representing Serializable objects. The following code fragment creates an XML document called "Test.xml" that contains an encoding of the supplied bean and all its properties:
XMLEncoder e = new XMLEncoder(
    new BufferedOutputStream(
        new FileOutputStream("Test.xml")));
e.writeObject(new JButton("Hello, world"));
e.close()

XMLEncoder works by cloning the object graph and recording the steps that were necessary to create the clone. This way XMLEncoder has a "working copy" of the object graph that mimics the steps XMLDecoder would take to decode the file. By monitoring the state of this working copy, the encoder is able to omit operations that would set property values to their default value, producing concise documents with very little redundant information.

How XMLEncoder Uses Persistence Delegates

Unlike for ObjectOutputStream, whose writeObject() methods are contained in the classes that are being written to the stream, it is possible to override the way objects of a given class are encoded by an XMLEncoder object. XMLEncoder contains a set of PersistenceDelegates, organized according to the class of object they encode. This internal map, from class to persistence delegate, can be queried and modified by the user to change the way that the encoder handles particular classes when it encounters them as part of a graph it is encoding. See the setPersistenceDelegate() method of Encoder for details on how to do this. The single public entry point for a PersistenceDelegate is its writeObject() method, and XMLEncoder does little more than look up the appropriate persistence delegate for the class of object being written and then call its writeObject() method.

Note that while the user is effectively given the ability to replace the writeObject() methods of all classes, there are no corresponding facilities for dealing wtih readObject() methods. This is because XML documents created by XMLEncoder are programs that are interpreted by XMLDecoder against a fixed set of semantics. There is no need to worry about readObject() methods -- because there aren't any!

When XMLEncoder has no PersistenceDelegate for a given class it uses an instance of the DefaultPersistenceDelegate class to provide a default encoding strategy. The DefaultPersistenceDelegate assumes the object is a bean that follows the idioms laid out in the JavaBeans specification. The DefaultPersistenceDelegate begins by calling the no-argument constructor of the appropriate class. It then uses the Introspector class to get a list of properties for the class and writes out each of the property values as a set of statements that are applied to the new instance. When the values of properties are different to their default values the Encoder is called recursively, to write out an encoding for each of the instances it contained as a property value. This typically generates output in the following style (given in Java here for familiarity):

JButton b = new JButton();
b.setText("Press, me");
b.setName("Button1");
...

If you are writing a class that you want to serialize with XMLEncoder, one easy way to make sure that all the state will be captured by this default procedure is to make the object follow the JavaBeans conventions. If a class has a no-argument constructor and all of its state is exposed in its properties, each of which is independent of the others, you do not need to write a persistence delegate -- the default persistence delegate will be able to write out all of the state in your object automatically.

Exposing Non-Standard Property Setter and Getter Methods

If a bean has a property whose getter and setter methods do not use the conventional names (are not prefixed with "get", "is", or "set") you can supply Introspector with a BeanInfo class that returns a PropertyDescriptor with the appropriate methods. XMLEncoder will then take account of the new property and use a method call in the XML archive instead of a property. See this Swing Connection article, which describes how properties and method calls are represented in the XML schema recognized by XMLDecoder.

Making Properties Transient

If you do not want a property to be written out, even if it has a non-default value, you can mark the property as "transient" in the BeanInfo for the class you are intending to save. The DefaultPersistenceDelegate checks for this property attribute and ignores any properties that are marked transient. The property will be omitted from the archive altogether and will take a default value when the archive is read. This code fragment makes the "text" property of the JTextField class transient:
BeanInfo info = Introspector.getBeanInfo(JTextField.class);
PropertyDescriptor[] propertyDescriptors =
                             info.getPropertyDescriptors();
for (int i = 0; i < propertyDescriptors.length; ++i) {
    PropertyDescriptor pd = propertyDescriptors[i];
    if (pd.getName().equals("text")) {
        pd.setValue("transient", Boolean.TRUE);
    }
}

Creating Custom Persistence Delegates

When an object's properties do not completely cover all of the state in an object or cover more of the object's state than is necessary for your application, you can use persistence delegates to ensure that the XML archives are written with just the right amount of information for the application. Note that because persistence delegates are used only in the writing part of this process, the XML output can be read by systems that do not have access to the persistence delegates used to create the output. In this sense, XMLEncoder is much more like a code generator than a conventional serialization system.

If an object has state not revealed in its properties and you can't change the API to expose this state through conventional JavaBeans idioms, you can write a persistence delegate to accommodate this information and leave the API of the class as is. The following sections discuss ways in which persistence delegates can be used to accommodate classes that are not, strictly speaking, beans.

 

Note: Due to a bug in 1.4.0, XMLEncoder can lose its references to custom persistence delegates when the VM runs low on memory. Thereafter, the custom persistence delegates won't be used. The workaround is to maintain strong references to all BeanInfos corresponding to classes that have custom persistence delegates.

The complete code for the workaround is shown in the report for bug #4646747.

Customizing Instantiation

When a class lacks a no-argument constructor, you need to provide a custom persistence delegate to instantiate the class. The technique you use depends on whether the class has public constructors and, if so, how its arguments are related to the properties of the instance it will create.

Constructors whose arguments are properties

The most common cases where one needs to modify the behavior of XMLEncoder to accommodate APIs that are not handled by the default persistence delegate are immutable classes, like Color and Font, that lack no-argument constructors. In these cases, where a public constructor exists that takes a set of property values as arguments, we can handle the classes by creating an instance of the default persistence delegate that knows the names of these properties and submitting it to the XMLEncoder object. For example:
XMLEncoder e = new XMLEncoder(System.out);    
e.setPersistenceDelegate(Font.class,
                         new DefaultPersistenceDelegate(
                             new String[]{ "name",
                                           "style",
                                           "size" }) );
e.writeObject(new Font( ... ));

Note: In practice, Colors, Fonts, and all the properties of all of the subclasses of java.awt.Component in the Java platform are already dealt with by private persistence delegates of XMLEncoder just like the preceding one -- so you don't need to worry about creating persistence delegates for them yourself.

Constructors with arguments that are not properties

If the constructor's arguments don't all map directly to properties, then you need to create a persistence delegate with a custom instantiate() method. For example:

Encoder e = new XMLEncoder(System.out);
e.setPersistenceDelegate(Integer.class,
                         new PersistenceDelegate() {
    protected Expression instantiate(Object oldInstance,
	                                 Encoder out) {
        return new Expression(oldInstance,
                              oldInstance.getClass(),
                              "new",
                              new Object[]{ oldInstance.toString() });
    }
});

The preceding implementation uses the special method name "new" to refer to the Integer class's string constructor.

Note: Like Colors and Fonts, Integers are already dealt with by private persistence delegates of XMLEncoder.

Factory methods

To accommodate instances of classes that have no public constructor but can be instantiated by a factory method, we can create a special-purpose persistence delegate by overriding the instantiate() method in either the PersistenceDelegate or DefaultPersistenceDelegate class. Here is a persistence delegate that can be used to deal with instances of the Method class:
class MethodPersistenceDelegate extends PersistenceDelegate {
    protected Expression instantiate(Object oldInstance, Encoder out) {
        Method m = (Method)oldInstance;
        return new Expression(oldInstance,
                              m.getDeclaringClass(),
                              "getMethod",
                              new Object[]{m.getName(),
                                           m.getParameterTypes()} );
    }
}

Note: Again, XMLEncoder already has a private implementation just like the one above for the Method class.

When returning an expression in an instantiate() method it is always a good idea to provide the instance that will be created by the expression when it is evaluated. (See the API documentation for Expression.) If we did not provide oldInstance to the expression before handing it back to the encoder, it would have to actually call the method to produce the value. Not only is this less efficient, it also it strictly incorrect in the case of methods (and constructors) that return a different instance each time they are called. So while submitting such a delegate would succeed in creating an archive (albeit a little more slowly) it might not correctly preserve the identity of relationships in the graph.

Customizing Initialization

When the properties returned by the Introspector do not cover all of the important state governing an object's behavior it is possible to augment (or replace) the property-based initialization of an instance with other initialization code that captures the information that would otherwise be lost. The java.awt.Container class is just such a class since, although its principle function is to contain java.awt.Components, these components cannot be restored by setting any of its properties. Instead, one must call one of the add() methods to install each of its children.

Because some other special cases apply to the initialization of a Container, we will use Swing's DefaultListModel as a simpler example that illustrates the same point. If we were to use the default persistence delegate on an instance of the DefaultListModel we would not record all the elements it contained. To address this we could use a special persistence delegate that exploits special knowledge of the API of the DefaultListModel.

class DefaultListModelPersistenceDelegate extends DefaultPersistenceDelegate {
    protected void initialize(Class type, Object oldInstance,
                              Object newInstance, Encoder out) {
        // Note, the "size" property will be set here.
        super.initialize(type, oldInstance,  newInstance, out);

        javax.swing.DefaultListModel m =
            (javax.swing.DefaultListModel)oldInstance;

        for (int i = 0; i < m.getSize(); i++) {
            out.writeStatement(
                new Statement(oldInstance,
                              "add", // Could also use "addElement" here.
                              new Object[]{ m.getElementAt(i) }) );
        }
    }
}

Because we call the initialization method of the superclass (DefaultPersistenceDelegate), this persistence delegate still records all of the properties of a DefaultListModel, in addition to all of the elements that the list model contains. Note that, unlike a field-based approach, the archives produced by this delegate rely only on the existence of a suitable "add" method in the target VM and therefore work even when the implementation that reads an archive stores those elements differently from the VM that wrote the archive.

We can make one more refinement to this persistence delegate to cover the unusual case where an instance of DefaultListModel is found as the property of another bean and is already initialized with some elements. In this case (which specializes to the preceding one) we just add those elements that are present in the instance we are trying to replicate and not present in the instance that we are given. (Code differences are in bold font.)

class DefaultListModelPersistenceDelegate extends DefaultPersistenceDelegate {
    protected void initialize(Class type, Object oldInstance,
                              Object newInstance, Encoder out) {
        super.initialize(type, oldInstance,  newInstance, out);

        javax.swing.DefaultListModel m =
            (javax.swing.DefaultListModel)oldInstance;
        javax.swing.DefaultListModel n =
            (javax.swing.DefaultListModel)newInstance;

        for (int i = n.getSize(); i < m.getSize(); i++) {
            out.writeStatement(
                new Statement(oldInstance,
                              "add",
                              new Object[]{ m.getElementAt(i) }) );
        }
    }
}

In this example we make use of the newInstance variable, which refers to an instance in exactly the state it will be as this file is read -- just as we are about to perform these initialization steps. This policy of examining the partially initialized instance to make sure we are accommodating any state that this new instance may already have is often unnecessary, but it is a good practice in persistence delegates for classes that may be subclassed. This technique is especially effective in the internal persistence delegate of the java.awt.Container class, since it removes the need for any of the Container derivatives in the Swing tool kit to include special-case code for handling their children.

Initialization without assumptions

There are some implicit assumptions in the preceding persistence delegate: mainly that all the elements in the newInstance will exist in the instance that is being archived (oldInstance). It is possible to write a persistence delegate that does not make any assumptions about the newInstance and will always perform exactly the operations required to initialize the newInstance -- regardless of its initial state. Persistence delegates like these are non-trivial to write and we have found delegates written in the style above to be more than adequate for the components in the AWT and Swing packages.

Persistence delegates for classes that implement List and Map

An internal persistence delegate for the AbstractList class has been provided that performs strictly correct initialization of the AbstractList class and its derivatives (including the java.util.Vector class) by not making any assumptions about initial state. The interested reader is recommended to look at the source code of the package-private java.beans.MetaData, which contains all the internal persistence delegates used by XMLEncoder -- including delegates that may be used for classes implementing the java.util.List and java.util.Map interfaces. These delegates are installed in XMLEncoder so that they will automatically be applied to all instances of java.util.AbstractList, java.util.AbstractMap, java.util.Hashtable, and their derivatives. They may be declared to apply to other classes implementing these interfaces as follows:

XMLEncoder e = new XMLEncoder(System.out);
e.setPersistenceDelegate(MyList.class,
                         e.getPersistenceDelegate(List.class));
e.writeObject( ... );

Registering for Exception Notifications

Both XMLEncoder and XMLDecoder catch exceptions and are typically able to recover from them, allowing the parts of the archive not affected by the exception to be written or read. You can find out more about any exceptions raised in the encoding and decoding processes by registering an ExceptionListener as follows:
XMLEncoder e = new XMLEncoder(System.out);
e.setExceptionListener(new ExceptionListener() {
    public void exceptionThrown(Exception exception) {
        exception.printStackTrace();
    }
});
e.writeObject( ... );

Note that when using an exception listener with XMLDecoder (as opposed to XMLEncoder) you should normally pass the exception listener as an argument to the decoder's constructor. Because the decoder decodes its input as soon as it is instantiated, installing an exception listener by modifying its exceptionListener property is usually too late to be useful.

Using the Owner Property

Both XMLEncoder and XMLDecoder have "owner" properties that can be used to link parts of the archive to objects that are not created by the archive. The owner may be of any type and, just as with the rest of the archive, methods are dispatched to it based on the run-time class of the owner that is submitted to the decoder (or encoder). It is possible to use the owner property to:
  • Call methods on the owner (or its properties) as the archive is being loaded.
  • Install the owner (or some property it contains) as a property of some UI element (typically as the target of an EventHander so that UI events can be used to call methods on the owner).
  • Set the properties of the owner to parts of the UI that require programmatic manipulation.
We provide an example that shows how to use all three of these features. The CreateUI class uses XMLEncoder to create an XML representation of the user interface called Factorize.xml. Once the XML description of the interface has been created, the CreateUI class is no longer required. The XML description can then be loaded by the Factorize class, which contains the logic that this application performs. To see this working, compile the CreateUI and Factorize classes, run the CreateUI class to generate the XML file, and then run the Factorize class.

Creating Internationalized Applications

There are two ways to create internationalized applications that use this XML encoding as the description of their user interfaces. The most general way is to create separate XML archives for each locale and place the archives in resource bundles so that the right user interface is loaded for each locale. The second way, possible when the user interface has the same structure in multiple locales, is to create a generic XML archive that uses references to items in a resource bundle instead of literal strings so that the user interface contains different strings when it is loaded in different locales.

The XML file ResourcesExample.xml is an example of the second way. It can be loaded with the standard XMLDecoder readObject() idioms -- see the API documentation for XMLDecoder. The XML archive, instead of containing explicit strings for its user interface components, draws the strings from a resource bundle using keys. You can see this archive create a localized user interface by placing the resources directory from the Stylepad application in your class path. (Stylepad is one of the "jfc" demos in the J2SE SDK.)

ResourcesExample.java shows how you can use XMLEncoder to generate the ResourcesExample.xml archive above from the object graph and the resource bundle -- by registering the contents of the resource bundle with XMLEncoder prior to calling its writeObject() method.

Note: The features required to write such files appeared first in 1.4 Beta 2. The code base of 1.4 Beta 1 supports reading XML archives that are written this way but does not support the constructs used here to create them.

Philip Milne, a former member of the Swing team, designed and implemented Long-Term Persistence for JavaBeans. A big fan of interactive GUI builders such as NeXT's InterfaceBuilder, he is happy to continue to answer questions relating to JSR-57. He can be contacted at: philip<AT>pmilne<DOT>net