Introducing Betwixt, Page 3
To convert a JavaBean to XML, you need an instance of the BeanWriter class. This class contains four separate constructors, allowing you to use an OutputStream (or one with a different encoding than the platform default), a Writer, or a default empty constructor that defaults to using System.out for its output. In this case, we've used a FileWriter, which outputs the results of the conversion to a file called output.xml.
The Ingredient class is created here with dummy data. Note that contrary to what we mentioned earlier, JavaBean-to-XML conversion doesn't require the presence of an empty constructor. Once we're ready with the bean we want to write, we write it out using the write method to the underlying stream. The first argument is the top-level element name, and the second argument is the bean to write. Finally, although Betwixt is configured to flush automatically for OutputStreams, it's a good idea to do so yourself in all cases.
To start the reverse process, we use the BeanReader class, which is really a wrapper around the Digester component. Before the XML document can be read in, Betwixt needs to know the toplevel element and the class it maps to. This is done using the registerBeanClass method. By registering the class, Betwixt creates an ObjectCreateRule behind the scenes for the internal Digester instance.
All that's left is for the BeanReader class to read in the XML representation. This is again delegated internally to the Digester instance. The resulting object that is created is an exact representation of the original Ingredient object.
With this example behind us, let's look at how you can read information about a target bean from an external file.
Using a .betwixt file
A .betwixt file is used to provide information about a target class. If Betwixt finds a .betwixt file in the CLASSPATH that matches the target class name, it doesn't use the default java.beans. Introspector and instead relies on the information about the bean in the XML file. Note that this file can be used in both bean --> XML and XML --> bean transformation. Any properties that you set in the code are overwritten if matching properties are found in this file. This implies that the .betwixt file has the final say in how Betwixt's transformation for a target class is configured. In this section, you'll see how to use this file.
The .betwixt file should be described in XML. This means that there must either be a schema or a Document Type Definition (DTD) for the valid structure of this file. Listing 5 shows the relevant DTD, which you can find in the resources section of the Betwixt source code.
Listing 5. DTD for .betwixt file
<?xml version='1.0' encoding='UTF-8'?> <!-- $Id: dotbetwixt.dtd,v 1.7 2003/08/24 16:58:54 rdonkin Exp $ --> <!-- The DTD for .betwixt files --> <!ELEMENT attribute EMPTY> <!ATTLIST attribute name CDATA #IMPLIED type CDATA #IMPLIED uri CDATA #IMPLIED value CDATA #IMPLIED property CDATA #IMPLIED > <!ELEMENT addDefaults EMPTY> <!ELEMENT element (attribute|addDefaults|element|text)*> <!ATTLIST element name CDATA #REQUIRED type CDATA #IMPLIED uri CDATA #IMPLIED value CDATA #IMPLIED property CDATA #IMPLIED updater CDATA #IMPLIED class CDATA #IMPLIED > <!ELEMENT text EMPTY> <!-- If you specify value, you cannot specify type or property --> <!ATTLIST text type CDATA #IMPLIED value CDATA #IMPLIED property CDATA #IMPLIED > <!ELEMENT hide EMPTY> <!ATTLIST hide property CDATA #REQUIRED > <!ELEMENT info (hide|element)*> <!ATTLIST info primitiveTypes (element|attribute) "attribute" >
The root element of the .betwixt file, even though it may not be obvious from the DTD, should be the info element; it has only one attribute, primitiveTypes. This attribute specifies whether primitive data types like String and int in the target class should be written out as elements or attributes.
Potential bug: Even though the DTD specifies that the default value for the primitiveTypes attribute is attribute, in reality the default seems to be element. By the time this is published, this bug may have been resolved.
Let's start by creating a .betwixt file for our Ingredient class. We'll use the same code as in the previous section, and we'll drop this ingredient.betwixt file in the CLASSPATH. This will ensure that it's read by Betwixt and used for writing output.xml from listing 4 (and for reading and creating an Ingredient object). Listing 6 shows the first draft of this file.
Listing 6 First draft of ingredient.betwixt
<?xml version='1.0' encoding='UTF-8' ?> <info primitiveTypes="attribute"> <element name='ingredient'> <addDefaults/> </element> </info>
The root element is info; it contains a single attribute called primitiveTypes. The root element in the output.xml file must be named ingredient. And, Betwixt must use java.beans.Introspector to do the rest of the work of looking at the Ingredient bean.
If you place this file in the CLASSPATH and rerun the code in listing 4, your output.xml file will look like this:
<ingredient alternate="None" key="POT" name="Potato"/>
The output is a single line with attributes for all the properties and the root element is ingredient.
To stop the printing of an element/attribute, use the element hide in the .betwixt file and specify the property that you want to suppress. This is shown in the second draft, in listing 7.
Listing 7. Stopping the output of an element in ingredient.betwixt using the hide element
<?xml version='1.0' encoding='UTF-8' ?> <info primitiveTypes="attribute"> <hide property="alternate" /> <element name='ingredient'> <addDefaults/> </element> </info>
You can use the hide element to suppress the printing of undesired properties. Note that the list of hide elements should appear before the element element. The output of using this file now looks like this:
<ingredient key="POT" name="Potato"/>
This effectively suppresses the alternate property. The result would be similar if primitiveTypes was set to element instead of attribute: Both key and name would be printed as elements, and alternate would be suppressed.
If you don't want default matching for your properties and elements/attributes, you can specify your own. You can specify as many properties you want to match and leave the rest to the default introspector by using the addDefaults element. Until now, we've left the matching of all the elements to the default introspector. Let's change this: listing 8 shows how.
Listing 8. Overriding the default matching behavior
<?xml version='1.0' encoding='UTF-8' ?> <info primitiveTypes="element"> <element name='ingredient'> <element name="key" property="key"/> <element name="name" property="name"/> <element name="alternate" property="alternate"/> </element> </info>
As you can see, the elements in the output.xml file are now explicitly matched to their respective properties in the Ingredient class. You could change this behavior to match these properties to any element that you wanted; this works in both directions of a transformation. You can also specify the method to use for the setting of these properties. Thus, instead of the default setXXX method, you can use any method that you wish to call, by using the updater attribute. For example, suppose you have a method calledJsetDifferentName instead of setName, and you want to use this method to set the name property. You change this in listing 8 as shown here:
<element name="name" property="name" updater="setDifferentName"/>
Now, each time Betwixt encounters the name element in the XML file, it will map that to the name property of the Ingredient object using the setDifferentName method. You can take this behavior one step further by using the attribute class, which allows you to use a different class than the primitive class for the instantiation of your property. Thus, if you want your String properties to be instantiated using a modified version of the String class called ModifiedString, you can use the following:
<element name="name" property="name" class="ModifiedString"/>
This is especially useful in cases where the type of a property is specified using an interface or an abstract class, and you want to use a specific implementation to create your object.