Sun Java Solaris Communities My SDN Account Join SDN
 
Article

XML JavaBeans Integration, Part 3

 
 

Articles Index

Integration, Part 3

This article completes the three-part series on the XMLBeans package (see Resources for the previous two articles in the series). Simply put, XMLBeans can transform a JavaBean in memory into an XML document, or can transform an XML document (of a particular form) into a running JavaBean. Figure 1 shows an extremely simplified schematic of the concept.

If you've experimented with XML and JavaBeans in this series, you'll have noticed that the XMLBeanWriter class we created in Part 2 is capable of creating XMLBeans XML files that the XMLBeanReader from Part 1 can't read. Some readers have also noted that the XMLBeans file format is somewhat complicated, and that controlling which properties are encoded in XML and which properties are ignored requires a lot of tedious coding. This month, we'll solve these problems by restructuring the XMLBeanReader and XMLBeanWriter classes. In the process, we'll simplify the file format while maintaining backward compatibility. Perhaps most interestingly, we'll simplify and generalize controlling XMLBeans properties by integrating XMLBeans with the core java.beans package.

Background reading

This article covers some intermediate-to-advanced JavaBeans topics, so you may need to do some background reading to get up to speed. The links to the articles described in this section appear in the Resources section.

First, I assume you've read and understand the first two articles (Parts 1 and 2) in this series. If you haven't read these articles, this month's article may not make much sense to you.

If you're new to XML, you might consider reading "XML for the absolute beginner," (JavaWorld April 1999). This article discusses JavaBeans property descriptors, property editor classes, and BeanInfo objects, topics previously covered in "The trick to controlling bean customization," and "Soup up your Java classes with customization." Once you have a passing understanding of these subjects, you'll be ready to tackle this final installment of the XML JavaBeans series.

As always, I shortened the code listings in this article by putting the code section under discussion into a scrolling list box. Since some readers dislike this convention, each code listing's caption includes a hyperlink to a printable HTML file. You can pop open a new browser and scroll this complete listing, if you'd like, or you can print the file and follow along on paper.

Improving XMLBeans

I find software to be in some ways like art: in practically every instance, as soon as I've completed a project, I'm aware of my work's weaknesses and how it could have been done better. The code we're going to discuss is a second attempt at creating an interface to read and write JavaBeans objects as XML documents. We won't spend a lot of time discussing the weaknesses of the previous design, but you'll see as we go along that the new implementation is cleaner and more flexible than the original.

This article introduces four major improvements to the XMLBeans package. The first involves general restructuring and simplification of the code, taking advantage of the concept that the value of any JavaBean property may be a primitive value, an object, or another JavaBean. The second improvement is an expansion of the customization of how XMLBeans read and write their properties in XML (or, more accurately, as DOM trees). Third, we give the programmer control over properties' visibility and behavior by integrating the XMLBeans package with the core java.beans package. And finally, we provide an alternate, less restrictive (and less well-defined) format for the XML produced and read by the package.

We'll start by going over the general restructuring improvement.

A value by any other name

The original XMLBeans implementation drew a distinction between JavaBeans properties that are "values" and those that are "beans." Value types were either Java primitive types (boolean, byte, char, short, int, long, float, or double) or any object type that is not primitive but can be constructed from a string (java.net.URL, for example). Bean types were explicitly encoded in XML as <JavaBean CLASS="<i>classname</i>">, and properties whose values were beans were processed differently from those that were not.

After writing the first version of the XMLBeans package, it became clear that there's not much of a difference between a property type that's a primitive value, one that's an object, or one that's a JavaBean. Primitive types require some special treatment, that's true, but the basic process for reading a JavaBean in terms of its properties is simple:

  • Create an instance of the JavaBean. The class name of the JavaBean appears in the Element that represents the bean.
  • For each property Element (an Element representing a property of the bean):
    • Determine the class of the property (by querying the PropertyDescriptor for the property)
    • Create an instance of the property class (we'll call this the property value since it will be the value to which the property is set)
    • Initialize the property-value instance using the contents of the property Element
    • Set the property using the property-value instance and the property-write accessor (the setter)

Likewise, the process for writing a JavaBean is straightforward:

  • For each property in the bean:
    • Get the property value from the bean by calling the property's read accessor (the getter for the property)
    • Convert the property value to a string
    • Add an element that represents the property and contains the string representation of it to the DOM tree you're creating

  • When all properties are encoded, write out the DOM tree as XML

You'll notice that, in the outline above, reference is made to property value without indicating whether the value is a bean, a primitive value, or an instance of some nonbean class. In the source code (which we'll discuss below), you'll see that these cases are handled separately and, in some cases, recursively. But the basic program structure reflects the concept that a property value is simply a property value, and the differences between the various kinds of properties (value, object, or bean) are just programming details.

The second major improvement—customization—is next.

Customization

Since XMLBeans is a utility package, meant to be used by a programmer, many of the features in the package are programming facilities, such as hook functions that allow a programmer to customize the package's behavior without doing violence to its operation.

One example of such a hook is the getAsDOM() method. In Part 2 of this series, I introduced a method in the XMLBeanWriter class to control how specific property types and JavaBeans are encoded as a DOM tree. The programmer has the option of specifying a method in a bean class called getAsDOM(). When XMLBeanWriter encounters a JavaBean with the getAsDOM() method, it defers to the bean itself to self-encode as a DOM tree. If a JavaBean class doesn't define a getAsDOM() method, the JavaBean is converted to XML in the standard way: by encoding all of its properties in XML.

The method DocumentFragment getAsDOM(Document d) by definition returns a DOM tree that represents its owner object. The XMLBeanWriter class recognizes this signature both for beans and for property-type classes.

As noted above, if a JavaBean contains a method called getAsDOM() (with the appropriate signature and return type), XMLBeanWriter will call that method and return whatever that method returned as the DOM representation of the object. If getAsDOM() doesn't exist, XMLBeanWriter loops through all of the bean's properties, encoding each property as a DOM tree. Each DOM tree that represents a property is added to the list of properties in the DOM tree representing the bean, until all properties are encoded. In addition to using getAsDOM to encode an entire bean, XMLBeanWriter always looks to see if each property type defines getAsDOM() and, if it does, uses that method to encode the property as a DOM tree.

So, for example, imagine we had a bean class called CustomerAccount, representing the information for a particular customer in an online sales system. The designer of the CustomerAccount class could choose to specify a method called getAsDOM(). When applied to a CustomerAccount object, the XMLBeanWriter will detect and call the getAsDOM() method. getAsDOM() will then return the custom DOM representation of the CustomerAccount JavaBean, instead of looking at all of the CustomerAccount's properties.

Furthermore, imagine that the CustomerAccount has a property called Address, of type CustomerAddress. If the CustomerAccount doesn't have a method getAsDOM(), but the CustomerAddress does, XMLBeanWriter will encode the Address property by calling CustomerAddress.getAsDOM().

Using getAsDOM() in either of these two places gives the bean designer a great deal of control over how the bean should be represented when the bean is converted to XML. In fact, at this point the designer has too much control because the existing version of XMLBeanReader doesn't know how to handle generic XML. As of the end of Part 2 of this series, it was possible to write JavaBeans with XMLBeanWriter that couldn't be read by XMLBeanReader.

Of course, the new version of the XMLBeans package removes this limitation, by allowing specification of a setAsDOM() method to match any getAsDOM() method. Just as XMLBeanWriter may use getAsDOM() to retrieve the DOM representation of a bean (for writing to an XML file), XMLBeanReader uses setAsDOM() to initialize a property value in a bean-object instance from the structure of a DOM subtree.

To provide more flexibility, an XMLBeans programmer may define getDOMSetterName() and/or getDOMGetterName(), which should return the name of a method to use in place of the names getAsDOM() or setAsDOM(). This allows more flexibility in naming, and also allows the object to decide at runtime which method to use for XML translation. This could be useful in a case in which there may be several alternate XML representations of an object, requiring several forms of getAsDOM() and setAsDOM(), and the application needs to be able to choose the translation method pair at runtime.

You may remember from Part 2 that it wasn't easy to prevent XMLBeanWriter from encoding properties. The only way to do so was to write a getAsDOM() method that encoded all properties as a DOM tree, excluding the properties not desired in the output. Though this technique produces the desired result, it requires a lot of unnecessary programming. The third major improvement to the XMLBeans package solves this problem (and others) by integrating XMLBeans with the java.beans package.

XML property descriptors and property editors

Typically, a JavaBean designer controls the visibility of bean properties to external agents by creating a BeanInfo object for the bean. One of the methods of the BeanInfo interface is PropertyDescriptor[] getPropertyDescriptors(), which returns an array of PropertyDescriptor objects, one for each property that the bean defines.

java.beans.Introspector, the class responsible for analyzing JavaBeans, identifies bean properties by first trying to find a BeanInfo object. If it finds one, it calls the BeanInfo's getPropertyDescriptors() method to get the list of properties. Only if this attempt fails does the Introspector itself analyze the class to determine properties.

For example, imagine a JavaBean ThingForSale with three properties: int weight, int age, and int price. A programmer could hide the price attribute by simply defining a getPropertyDescriptors() method in the BeanInfo class for the bean ThingForSaleBeanInfo that returns PropertyDescriptor objects only for age and weight. (For the fine details of how to do this, see my "The trick to controlling bean customization," (JavaWorld November 1997).)

A PropertyDescriptor indicates not only the property, but also the property accessor methods (setter and getter methods). As described above, when analyzing a bean, the Introspector first checks to see if it can get a list of PropertyDescriptors from the BeanInfo. If that fails, the Introspector analyzes the bean class, building a list of methods that match the signatures <i>Type</i> get<i>Property</i>() and <i>void</i> set<i>Property</i>(<i>Type</i> value). From this list of methods, the Introspector then creates its own PropertyDescriptor list.

If you think about it for a moment, it should be pretty clear to you that the getAsDOM() and setAsDOM() in a JavaBean could actually be used as setter and getter methods for bean properties. Instead of expressing the property in terms of its actual property type, though, these two methods get and set the property in terms of a DOM tree. In fact, each bean property could have its own DOM setter and DOM getter—accessors for that property that set the property value as an Element, and get the property value as a DocumentFragment, respectively. We can even define a naming convention for them, just as we can for normal property accessors:

  • A method with the signature <i>Type</i> get<i>Property</i>AsDOM() defines the "DOM getter" or "DOM read method" for a property <i>Property</i> of type <i>Type</i>.
  • A method with the signatureroperty <i>Property</i> of type <i>Type</i>. <i>void</i> set<i>Property</i>AsDOM() defines the "DOM setter" or "DOM write method" for a property <i>Property</i> of type <i>Type</i>.

In addition, we can create a new subclass of java.beans.PropertyDescriptor, called com.javaworld.JavaBeans.XMLBeans.XMLPropertyDescriptor, which allows the programmer to define XML property descriptors from within the existing BeanInfo framework. Since a XMLPropertyDescriptor is a PropertyDescriptor, it will work properly anywhere in the role of a PropertyDescriptor. In addition, when XMLBeanReader or XMLBeanWriter want to know a JavaBean's properties, they can simply get the property list from the bean's BeanInfo. Any PropertyDescriptor that is an instanceof XMLPropertyDescriptor will contain a DOM setter and/or DOM getter method for use in converting that property to or from the DOM representation.

By creating the new XMLPropertyDescriptor type, we allow bean developers to specify the methods used to access properties as DOM trees. If the developer doesn't provide a BeanInfo object, XMLBeanReader and XMLBeanWriter search for methods matching the naming convention outlined above.

The second way that the new XMLBeans package integrates with java.beans is its use of property editors for type conversions. The java.beans.PropertyEditor interface defines three methods of interest to us:

  1. void setValue(Object o) —sets the object to be edited
  2. void setAsText(String text) —sets the value of the current object, based on the contents of the string
  3. String getAsText() —encodes the current object as string and returns the result

These methods interest us because they allow us to convert an object to and from a string in a general way, and in XML, everything is encoded as a string. When XMLBeanReader determines that a particular string (the value of a Text node) is a string representation of a property, it constructs a value of the property's type. For example, if a JavaBean has a Date property BirthDate, XMLBeanReader will construct a Date object to pass to the setter method for the BirthDate property. But how can it convert a String to a Date? Java Date objects don't have a constructor that takes a String as an argument. Are we foiled?

Of course we aren't. If a DateEditor exists, or if the PropertyDescriptor for the Date object specifies a custom editor, XMLBeanReader simply creates a property editor of the appropriate class, hands the newly-created Date object to the editor, and calls setAsText(). XMLBeanReader defers conversion of objects to and from a string representation to the PropertyEditor for the property, or (if the property doesn't have a custom editor), for the property type. This idea is extremely cool, because it gives the programmer complete control of the string format of each property, on a property-by-property basis. A custom PropertyEditor could be written to base64-encode a binary object. Another could encrypt and decrypt sensitive information. The text representation of a property is now entirely under programmer control, and in a way that is seamlessly integrated with the java.beans core.

The final improvement to XMLBeans is an optional simplification of the file format.

Simpler XML

TThe file format for XMLBeans is precise, but it doesn't lend itself easily to processing general XML. The original XMLBeans language consisted of a <JavaBean> element, with a single attribute called CLASS. Inside the <JavaBean> element was a <Properties> element, which itself contained a list of <Property> elements. Each <Property> element had a NAME attribute, and contained either a String, from which the property could be constructed, or another entire <JavaBean> (if the property's type was a JavaBean).

That's a pretty simple language. The DTD (data type definition) for this tiny language appears in Listing 1 below. (For an introduction to DTD, see my April 1999 JavaWorld feature article "XML for the absolute beginner.")

  <!ELEMENT JavaBean (Properties)>
<!ATTLIST JavaBean CLASS CDATA>
<!ELEMENT Properties (Property*)>
<!ELEMENT Property (#PCDATA)>
<!ATTLIST Property NAME CDATA>

Listing 1. The DTD for the original XMLBeans code (Parts 1 and 2)

This DTD tells the XML parser how to check the JavaBean's XML for correctness before it tries to process it. If you use a validating parser with this DTD, you can be certain that any XML tree you read from the file follows the rules for a well-formed XMLBean.

The file format is a little inflexible, though. You may have noticed that nobody except me actually uses this particular dialect of "JavaBean XML." What if you wanted to parse any XML document and convert it into a JavaBean?

The new XMLBeans package does just that. While the JavaBean XML language defined above will still work, the XMLBeanReader class now accepts any well-formed XML, and tries to build a bean out of what it finds. It does this by assuming that:

  1. The top-level Element of the input XML is a JavaBean, and the class name is either the element tag, or the value of that element's CLASS attribute, if set
  2. Every Element contained inside of the top-level Element represents a property of the bean, whose name is given by the element tag
  3. If a property element (one of the elements from assumption two, above) contains a nonempty Text node, then the property value can be constructed from that string
  4. If a property element (from assumption two) contains other Elements, then the property is a JavaBean, and the value is the result of loading that element subtree as a JavaBean
  5. The list of properties may optionally be enclosed in a Properties element, but need not be

So, for example, let's say we had a JavaBean class called Player (as you know we do, from previous columns). Some of the properties are JavaBeans, and so also have properties. The JavaBean classes and properties in our example are as follows:

Figure 2. Player properties and their types
Class Property Property type Comments
Player name class PersonName
Player stats class Statistics
Player number int
Player grades class Grades
PersonName first String
PersonName last String
Statistics year int
Statistics atbats int
Statistics runs int
Statistics hits int
Statistics homeruns int
Statistics runsbattedin int
Statistics strikeouts int
Grade grade(i) String indexed property
Grade classDesc(i) String indexed property

In our original XML dialect, using the DTD in Listing 1, the first few lines of an XMLBean might look something like Listing 2:

<JavaBean CLASS="Player">
  <Properties>
    <Property NAME="Name">
      <JavaBean CLASS="PersonName">
        <Properties>
          <Property NAME="First">Benjamin</Property>
          <Property NAME="Last">Hur</Property>
        </Properties>
      </JavaBean>
    </Property>
    <Property NAME="Number">12</Property>
    <!-- and so on... -->
  </Properties>
</JavaBean>

Listing 2. Part of a sample Player bean encoded in JavaBean XML

Encoding the same information in the new format is much clearer, as in Listing 3:

<Player>
  <Name CLASS="PersonName">
    <First>Benjamin</First>
    <Last>Hur</Last>
  </Name>
  <Number>12</Number>
  <!-- and so on... -->
</Player>

Listing 3. The same information as in Listing 2, encoded in the new, simpler format

Notice that almost all of the XMLBeans-specific markup is gone. The only thing that remains is the CLASS attribute on the Name property.

Listing 3 exemplifies all of the following rules for the new syntax except for the fifth:

  1. The top-level element is Player, so this object is a JavaBean of class Player. The class name, Player, comes from the tag name.
  2. Player has properties Name and Number (and probably some others, not shown), since these are the child elements of the Player element.
  3. The Number property contains only a nonempty text element, so its value is created from the string "12" (and converted to the appropriate type).
  4. The Name property contains other Element s, so the Name property is a JavaBean (whose class is PersonName, given by the CLASS attribute).
  5. All the nodes inside the Player node could have been enclosed in a <Properties> node.

A couple of minor details about these new syntax rules bear discussion. First, why did we maintain the CLASS attribute? Why not just require the tag name to be the class name? There are two reasons for this decision. First, the optional CLASS attribute provides for unambiguous identification of the property class. What if there were two or more classes called Player in the CLASSPATH? Which one should the XMLBeanReader choose? Second, and more importantly, the class attribute can be used to identify which subclass to use if a property type is abstract. What if PersonName were an abstract base class, with subclasses of MarriedName, MaidenName, and NickName? Which class should XMLBeanReader use when it creates the new JavaBean?

The <Properties> element could have been eliminated entirely; and indeed, it is optional. The reason I left it in the syntax is for future expansion. Currently, the only thing stored about XMLBeans are the JavaBeans properties. In the future, though, we may want to also store event sets, cached data, digital signatures, or other information about the bean that is not available from the properties. The <Properties> element allows us to indicate which subnodes of the bean are properties. In this example, we don't need it, because all we currently process is the property list.

I've described the improvements to XMLBeans. Let's look at some examples.

Sample bean classes

The sample beans I include in the source archive are the same ones I've used throughout this series. The XMLBeans package allows customizable encoding and decoding of JavaBeans to and from an XML format. These sample beans are not part of the XMLBeans package; instead, they were written exclusively to show how XMLBeans would operate on actual beans. Since I've modified them to demonstrate how XMLBeans customization works, I'll go through the classes and describe what they do, and what XMLBeans customization features they use. The sample bean class Player represents a team member in a high school sport; in this case, baseball (because the Statistics object encodes baseball statistics). The name of each class is linked to a printable source file for that class.

In addition, there are sample XML files, Bean1.xml and Bean2.xml, which show examples of a Player bean encoded in the old and new XML format. You can use them as input to the main() methods of the XMLBeans classes.

Class Player
The Player class represents a single baseball player. It has the following properties:

  • PersonName name —the player's name
  • Statistics stats —the player's statistics for this sport
  • int number —the player's number on the team
  • String highSchool —the name of the high school the player attends
  • Grades grades —the player's grade record.

The properties name, stats, and grades are all JavaBeans themselves, the property number is a primitive value, and highSchool is a String object (a nonprimitive, nonJavaBean object). They exist to demonstrate how XMLBeanWriter and XMLBeanReader encode and decode each kind of property.

Class Statistics
The Statistics class object contains baseball statistics for the Player. The names of the properties (year, atbats, runs, hits, homeruns, runsbattedin, and strikeouts) are all of type int. Two of them are handled specially:

  • hits—For no reason in particular, I decided to encode the number of hits the player received in a season in hexadecimal. int properties are usually encoded in decimal, but getHitsAsDOM() and setHitsAsDOM() customize the encoding of the Statistics bean's hits property. These two methods essentially allow the Statistics class to control how the property hits is encoded and decoded; in this case, they encode and decode the property in hexadecimal. When XMLBeanReader sets hits in the bean it is creating, it detects the existence of Statistics.setHitsAsDOM(), and passes the corresponding element to that method instead of calling the standard setter. Likewise, when XMLBeanWriter creates a DOM representation of Statistics, it notices that Statistics defines getHitsAsDOM(), and defers encoding hits as a DOM tree to that method.
  • strikeouts—To protect the privacy of our teenage athlete, I've decided to encrypt the strikeouts property. If you look at the code, you'll notice that the strikeouts property has no normal JavaBeans accessor methods. In this sense, it's not truly a property at all! You'll notice, however, that the print() method of the object does display the number of strikeouts in its report, so you can see that the quantity was correctly decoded. The methods getEncryptedStrikeouts and setEncryptedStrikeouts get and set the property as a DOM tree, and encrypt and decrypt the quantity, so that nosy people reading the XML won't know what the property value means. (The encryption is very easy to figure out, but you get the point.)

    How do XMLBeanReader and XMLBeanWriter know to use this method? They get the information from theStatisticsBeanInfo class, which has a method called getPropertyDescriptors(). The property descriptor for the strikeouts property is an XMLPropertyDescriptor (discussed below in the section entitled "Class XMLPropertyDescriptor"). In short, the XMLPropertyDescriptor specifies that the DOM setter for strikeouts is setEncryptedStrikeouts(), and the DOM getter for strikeouts is getEncryptedStrikeouts. XMLBeanReader and XMLBeanWriter use the property descriptor list returned by getPropertyDescriptors to determine how to get and set properties, and choose DOM accessors over traditional accessors when possible.

Class PersonName
The PersonName class represents a person's first and last names. It has no DOM customization and exists as an example of how XMLBeans reads and writes objects that know nothing of XMLBeans.

Class Grades
The Grades class represents a Player's academic history. It's particularly interesting because two of its properties— classDesc and grade—are indexed properties, and XMLBeans doesn't currently handle indexed properties. To get around this limitation, Grades defines getAsDOM() and setAsDOM(), which are totally responsible for encoding and decoding the entire Grades bean as XML. There's a third property, gradePointAverage, which is interesting because it is read-only, and is calculated from the values in the list of grade values. This property isn't even encoded in the XML, because to do so would be redundant. It simply uses the values of the grade property to calculate an American-style grade average (on a scale from 0 to 4).

Class PlayerBeanInfo
The PlayerBeanInfo class specifies to the XMLBeansPackage the properties of the Player bean. It extends the XMLBeans class XMLSimpleBeanInfo (see class XMLSimpleBeanInfo below). It shows an example of a standard BeanInfo object specifying a list of traditional (that is, nonXMLBeans) properties.

Class StatisticsBeanInfo
Another subclass of XMLSimpleBeanInfo, StatisticsBeanInfo specifies Statistics properties. The interesting thing about it is that, in getPropertyDescriptors(), it specifies an XMLPropertyDescriptor. To JavaBeans containers other than XMLBeans, an XMLPropertyDescriptor looks exactly like a PropertyDescriptor, for the simple reason that it is one (it's a subclass). But it supplies additional information (the DOM accessors) to XMLBeans.

Now that we've looked at what the new implementation does, let's see how it works.

A look at the code

The code for XMLBeanReader is the more difficult of the two methods in the package, so we'll tackle it first. You may download the source files in a variety of formats in the Resources section at the end of the article.

XMLBeanReader

Let's look at XMLBeanReader by tracing through its methods. The main() method, which I'll not bother to show here, simply passes a filename to readXMLBean(), and then prints the resulting JavaBean by calling the returned object's print() method (if it has one).

readXMLBean()
Method readXMLBean() comes in three flavors, only one of which (the one that takes a Reader as an argument) actually does any work. readXMLBean(), seen in Listing 4 below, creates an XML parser, reads the input XML file, identifies the first element of the document read, and calls instantiateBean() to create an instance of the bean and initialize it. This method parses XML into a DOM tree by calling a canned parser. I use IBM's XML4J (see the link in Resources), but you could choose another parser and the port would be very easy.

816  /** Read a bean's state from a character stream
817  * @param r The Reader from which to read the
     * JavaBean's state.
818  * @return Object The newly-created, 
     * initialized JavaBean.
819  * @exception IOException
820  * @exception ClassNotFoundException
821  * @exception IntrospectionException
822  */
823  public static Object readXMLBean(Reader r)
824   throws IOException, ClassNotFoundException, 
      IntrospectionException {
825 
826   // Read document from XML file
827   String sParserClassname = "";
828   Parser parser = null;
829 
830   // Create a SAX parser
831   try {
832   parser = ParserFactory.makeParser(
      "com.ibm.xml.parsers.NonValidatingDOMParser");
833   } catch (Exception exc) {
834      System.err.println("Exception");
835      exc.printStackTrace();
836   }
837    
838   P("Created parser");
839 
840   // Run the SAX parser against the input stream
841   try {
842      parser.parse(new org.xml.sax.InputSource(
r));
843   } catch (SAXException sx) {
844      System.err.println("Exception: " 
           + sx.toString());
845      sx.printStackTrace();
846   } catch (Exception ex) {
847      System.err.println("Threw " 
           + ex.getClass().getName());
848      ex.printStackTrace();
849   }
850    
851   // Since we know the SAX parser is also 
      // a DOM parser,
852   // we can ask it for its resulting document.
853   Document d = (
        (NonValidatingDOMParser)parser).getDocument(
        );
854   P("Got document " + isNull(d));
855 
856   Element eJavaBean = d.getDocumentElement();
857   P("eJavaBean is " 
        + isNull(eJavaBean));
858    
859   Object o = instantiateBean(eJavaBean);
860 
861   return o;
862  }

Listing 4. readXMLBean() parses XML and calls instantiateBean()

instantiateBean()
Method instantiateBean() (Listing 5) simply gets the class name of the bean and then loads the bean as if it were a property value of some other bean by calling loadValue(), the real workhorse method of the package.

477  /** Instantiate the JavaBean, and set all of 
     * its properties
478  * The element must be of type "JavaBean"
479  * @param eJavaBean The DOM Element object 
     * representing the JavaBean.
480  * It is the root of a DOM document tree that 
     * contains the information
481  * used to instantiate and initialize the 
     * JavaBean. The element's
482  * tag must be JavaBean.
483  * @return java.lang.Object
484  * @exception IOException
485  * @exception ClassNotFoundException
486  * @exception IntrospectionException
487  */
488  protected static Object instantiateBean(
       Element eJavaBean)
489  throws IOException, ClassNotFoundException, 
     IntrospectionException {
490 
491  // Load the value from the node
492 Object jb = loadValue(classOfBean, eJavaBean, null); 494 495 // Return the newly-instantiated JavaBean. 496 return jb;
497 }

Listing 5. instantiateBean() gets the bean class and loads the bean as a single value

loadValue()
Method loadValue() (Listing 6) is responsible for loading all of the properties of the JavaBean whose class it receives. It always receives the class of the object to create and an Element that is the root of a DOM subtree that encodes the value. There's also a PropertyDescriptor that may bear information about the value and how to set it. If the value to return isn't a property of any other object (which is the case only if it's the top-level JavaBean being encoded; see the call in instantiateBean() above), the PropertyDescriptor is null.

565 /**
566  * Load a value from an element node. The
     * resulting value will either be a
567  * JavaBean or a primitive wrapper object;
     * in either case, the result is almost
568  * always passed to a setter method. If the
       PropertyDescriptor is not null,
569  * then this object is a property of another
     * object, and the descriptor describes it.
570  * @return boolean
571  * @param bean java.lang.Object
572  * @param node org.w3c.dom.Node
573  */
574  public static Object loadValue(
       Class objectType, Element element,
       PropertyDescriptor pd) {
575      Object value = null;
576      Pe("Loading element "
           + element.getTagName() 
           + " for object " 
           + objectType.getName());
577 
578  // If this is a primitive property, we want to
     // return an object
579  // of the appropriate type for the property
     // descriptor's setter method
580  if (pd != null) {
581    if (objectType.isPrimitive()) {
582      value = getPrimitive(objectType, element);
583      return value;
584      }
585    }
586 
587  // Create an instance of this bean. This 
     // will be our return value.
588  try {
589    value = Beans.instantiate(null, 
         objectType.getName());
590    } catch (Exception ex) {
591       return null;
592    }
593 
594  // First, find out if this object knows how to
     // read itself
595  // from a DOM tree. If it does, we simply
     // defer to the object
596  // itself, and we have no further work to do.
597  Method domSetter = getDOMSetter(value, null);
598  if (domSetter != null) {
599    try {
600      domSetter.invoke(
           value, new Object[] {element});
601       } catch (Exception ex) {
602          Pe("loadValue:");
603          Pe(ex.getClass().getName());
604          ex.printStackTrace();
605       }
606       return value;
607    }
608 
609  // Get the first child of the element that
     // is either nonwhite text (in
610  // which case we try to initialize the
     // value from that); or a noncomment
611  // nonterminal node, in which case the value
     // must be a bean.
612  Node n =Util.getFirstInterestingChild(element);
613 
614  // If the syntax is <
     // Property NAME="foo"><
     // JavaBean CLASS="bar">...,
615  // then we want to look under the <
     // JavaBean> node for properties,
616  // not under the <Property> node.
617  Element topElement = element;
618  if (n instanceof Element && 
       ((Element)n).getTagName().equals("
       JavaBean")) {
619       topElement = (Element)n;
620  }
621 
622  // If n is null, initialize value with a  
     // blank string.
623  // If n is Text, initialize value with the 
     // value of the text.
624  String scalarValue = null;
625  if (n == null) {
626     scalarValue = "";
627  } else {
628     if (n instanceof Text) {
629       scalarValue = ((Text) n).getData();
630     }
631  }
632 
633  // If scalarValue is not null, then we want to
     // set the value from
634  // a string. This makes it easy for objects to
     // simply serialize
635  // themselves to flat strings, and then
     // deserialize themselves
636  // back from those strings. Essentially, the
     // PropertyEditor works
637  // as a tiny parser for the string value.
638  if (scalarValue != null && pd != null) {
639       // Use the property editor to initialize
          // the value.
640       if ((value = loadValue(objectType, 
            scalarValue, pd)) != null) {
641           return value;
642       }
643       Pe("loadValue() failed for property 
          " + pd.getName() + ", value = 
          '" +
          scalarValue + "'");
644    }
645 
646    // Since the object didn't know how to set
       // itself from a DOM,
647    // and the DOM that represents it contains
       // multiple nodes, we're
648    // going to have to enumerate all of the
       // properties. Assume that
649    // each subnode is a property, creating and
       // initializing a value for
650    // each one, and then using the property 
       // setter to set that value.
651    // If a <Properties> node exists in my
       // subnodes, that's the list of
652    // my properties, instead of all  
       // of my subnodes.
653    Node propertyList = topElement;
654    NodeList nl = topElement.getChildNodes();
655    int i;
656    for (i = 0; i < nl.getLength(); i++) {
657       if (nl.item(i) instanceof Element) {
658          Element e = (Element) (nl.item(i));
659          if (e.getTagName().equals(
               "Properties")) {
660             propertyList = e;
661             break;
662          }
663       }
664    }
665 
666  // Introspect object to get properties, then
     // create a hash table
667  // with property names as keys and descriptors
     // as contents
668  // Use the introspector to get the property
     // descriptors
669  // for this bean.
670  PropertyDescriptor[] pds = 
       getPropertyDescriptors(value.getClass());
671 
672  // Create a hash table of property names
     // and property descriptors
673  Hashtable htpd = new Hashtable();
674  for (i = 0; i < pds.length; i++) {
675       htpd.put(pds[i].getName().toLowerCase(), 
            pds[i]);
676  }
677 
678  // Now iterate through all of the properties
     // in the propertyList,
679  // loading each one recursively, and
     // passing the returned value
680  // to the setter method.
681  nl = propertyList.getChildNodes();
682  for (i = 0; i < nl.getLength(); i++) {
683 
684  // Get the name of the property,
     // ignoring rubbish
685  String propertyName =
       getPropertyName(nl.item(i));
686  if (propertyName == null)
687    continue;
688 
689  p("Property name = '" 
       + propertyName + "'");
690       
691  // Must be an element, since it wouldn't 
     // have a propertyName if not
692  Element e = (Element) (nl.item(i));
693 
694  // Get the property descriptor for the property,
     // ignoring
695  // if there's no descriptor for it
696  PropertyDescriptor thispd = (PropertyDescriptor)
     (htpd.get(propertyName.toLowerCase()));
697  if (thispd == null) {
698    Pe("Couldn't get PropertyDescriptor
         for property " + propertyName);
699    continue;
700  }
701 
702  // See if either the property descriptor or 
     // the bean knows how
703  // to set the value as a DOM tree
704  Method mDOMSetter = 
       getDOMSetter(value, thispd);
705 
706  if (mDOMSetter != null) {
707  P(" DOM Setter = "
       + mDOMSetter.getName());
708  try {
709    mDOMSetter.invoke(value, new Object[] {e});
710     continue;
711  } catch (Exception ee) {
712      Pe("Exception occurred while 
           setting " + propertyName + " as
             DOM: " + 
               ee.getMessage());
713              ee.printStackTrace();
714      return null;
715     }
716       } else {
717          p(", no DOM setter");
718       }
719 
720  // Call the setter directly. To do this, 
     // we have to get the setter method for
721  // the property, create an instance of its 
     // property type, and
722  // try to recursively loadValue(
) a value for it.
723  // That should handle both properties that are
     // JavaBeans
724  // and properties that can somehow be 
     // constructed from a string.
725  Method mSetter = thispd.getWriteMethod();
726  if (mSetter != null) {
727    P(", Setter " 
         + mSetter.getName());
728    try {
729      // Get the class of the property
730      Class cValue = getPropertyType(e, thispd);
731      Object setterArg = null;
732 
733      // The setter argument is the object 
         // referred to by the
734      // current element e. Calling loadValue() 
         // recursively here
735      // returns the argument for the setter.
736      if ((setterArg = 
           loadValue(cValue, e, thispd)) != null) {
737          mSetter.invoke(
               value, new Object[] {setterArg});
738        } else {
739           Pe("Couldn't set " 
                + cValue.getName() + " " + 
                  thispd.getName());
740          }
741      } catch (Exception ex) {
742          Pe("Couldn't create or set " 
               + thispd.getPropertyType().getName()
                 + " for property " 
                   + pd.getName());
743          ex.printStackTrace();
744        }
745       } else {
746          P(", Setter null");
747       }
748    }
749 
750    // Return the value we've just created.
751    return value;
752 }

Listing 6. loadValue() creates value instances and initializes them

Referring to the source, loadValue() performs the following tasks:

Lines Description
578-585 A primitive property is encoded by getPrimitive() (see below). A property that isn't primitive must have a default constructor, so we use java.beans.Beans.instantiate() to create an instance for the result. (Using this method isn't entirely correct, since the result may be a nonbean object. The code should check if the result class is a bean or not, and use new if it isn't. Feel free to fix that here.)
 
588-591 Create the "value" to be initialized from the Element. The value is simply an instance of the type to be returned.
 
594-607 If the object knows how to initialize itself from a DOM tree (that is, if the value class defines setAsDOM()), it simply passes the DOM tree to the DOM setter method, and the bean is created. The getDOMSetter() method returns a Method object representing the DOM setter for the property, or null if none could be found. If a DOM setter was found, this section invokes it and returns. getDOMSetter() (see source file) finds a method in one of two ways: (1) if pd is an XMLPropertyDescriptor, it returns the DOM setter from the descriptor (by calling the descriptor's getDOMWriteMethod()); or (2) if the value's class defines get<i>Propertyname</i>AsDOM(), that method is returned.
 
609-620 The contents of the value node may be one of two things. It may be a simple text string, in which case, the property value must be constructed from a string, like this: <Size>12</Size> or, it may be a list of other Element nodes defining a list of properties, like this: <Size>12</Size>  <Color>Red</Color>
White space in the input shows up as Text nodes containing white space, and there may also be Comment nodes sprinkled through the XML. getFirstInterestingChild() is a utility method that finds the first child that is either a nonempty Text node, or any other node type that's not a Comment.
 
622-644 If the object we're creating is to be initialized from a Text node (that is, a String), we call another form of loadValue() that specializes in initializing an object from a String (described below).
 
666-752 This code is the standard method for creating the XMLBean in the absence of any customization. The object being created is introspected to get a list of its properties, and then a hash table is created that indexes property names against their corresponding property descriptors. (These are the properties of the created value.) Since each child of the current Element is a property of the value being created, the code loops through all of this node's child elements, getting a value for each one (via a recursive call to loadValue()), and then setting it with the setter method of that property, called against the instance we're creating. It's a little confusing at first. We're simply calling loadValue() recursively to get the value of each property of the value currently being created. The Player bean, for example, needs to call loadValue() to get the values for its stats, grades and name properties, and it uses loadValue() to do so.

loadValue(), again
The second version of loadValue() (lines 526-563 in XMLBeanReader.java, concerns itself with creating only those values that can be constructed from String objects. It creates a new instance of the value class, uses the PropertyEditor for the value class to initialize the value, and returns the result.

getPrimitive()
Primitive types are a bit of a bother, since they need a wrapper class in order to be passed to a Method object (which is our only way of accessing a setter). getPrimitive() takes the primitive Class object and constructs an appropriate wrapper object, using the String from the DOM tree to initialize the value. The wrapper is created by getPrimitiveSetterArg(), which is interesting if you want to see how to construct an object when you don't even know the object's class at runtime: you simply get the constructor and invoke it!

The other methods in the XMLBeanReader class are simple utilities. Understand the first form of loadValue(), and you'll have no trouble understanding how the whole class works.

XMLBeanWriter

XMLBeanWriter creates a DOM tree that represents a JavaBean. The resulting DOM tree can then be written to an XML file. XMLBeanWriter is much simpler than XMLBeanReader. The only method that does any interesting work is getAsDOM(), which comes in two forms: one for a bean, and one for an bean's properties.

getAsDOM()
Listing 7 contains the getAsDOM() method used for entire beans. The other version of getAsDOM() returns a DocumentFragment that encodes a property and is used by the first version. A description of the second form of getAsDOM() follows Listing 8.

022 /**
023  * Build a DOM DocumentFragment representing 
     * the class and properties of
024  * a JavaBean.
025  * @return org.w3c.dom.DocumentFragment - 
     * the document representing the JavaBean.
026  * @param doc Creates nodes using this 
     * document as a factory.
027  * @param bean The JavaBean for which we 
     * want the DOM tree
028  * @exception java.beans.IntrospectionException 
     * An error occurred during
029  * introspection of the JavaBean.
030  */
031  public static DocumentFragment 
       getAsDOM(Document doc, Object bean) throws 
       IntrospectionException, 
       InstantiationException,
       IllegalAccessException {
032 
033 	// Create the fragment we'll return
034 	DocumentFragment dfResult = null;
035 
036 	// Analyze the bean
037 	Class classOfBean = bean.getClass();
038 
039 	// See if the bean has a custom name for its 
        // DOM setter
040 	String nameOfGetter = "getAsDOM";
041 	try {
042 	  Method mNameGetter = 
            classOfBean.getMethod(
            "getDOMGetterName",
            new Class[] {  });
043           nameOfGetter = 
                (String) (mNameGetter.invoke(bean, 
                  new Object[] {}));
044 	} catch (Exception ex) {
045 	    ; // Ignore exceptions—this 
              // either works or it doesn't
046 	}
047 
048 	// If the bean knows how to encode itself 
        // in XML, then
049 	// use the DOM document it returns.
050 	Method mGetAsDOM = null;
051 	try {
052 	  mGetAsDOM =
            classOfBean.getMethod(nameOfGetter,
              new Class[] {
                org.w3c.dom.Document.class });
053 		if (mGetAsDOM != null) {
054 			dfResult =
                          (DocumentFragment) 
                          (mGetAsDOM.invoke(bean,
                          new Object[] {doc}));
055 			return dfResult;
056 		}
057 	} catch (Exception e) {
058 		; // Ignore exceptions
059 	}
060 
061  // If we found a DOM read method, invoke it,
     // and we're done.
062  if (mGetAsDOM != null) {
063  try {
064    dfResult =
         (DocumentFragment) 
         mGetAsDOM.invoke(bean, 
         new Object[] {doc});
065    return dfResult;
066   } catch (Exception e) {
067 	; // Ignore exceptions
068 	}
069  }
070 
071  // Get a BeanInfo for the bean.
072  BeanInfo bi = 
     Introspector.getBeanInfo(classOfBean);
073 
074  // If the bean doesn't know how to encode
     // itself in XML,
075  // then create a DOM document by
     // introspecting the bean and
076  // inserting nodes that represent the
     // bean's properties.
077  if (dfResult == null) {
078  dfResult = doc.createDocumentFragment();
079  PropertyDescriptor[] pds =
       bi.getPropertyDescriptors();
080 
081  // Add an Element indicating that this
     // is a JavaBean.
082  // The element has a single attribute, 
     // which is that Element's class.
083  Element eBean = 
       doc.createElement("JavaBean");
084  dfResult.appendChild(eBean);
085  eBean.setAttribute("CLASS",
     classOfBean.getName());
086  Element eProperties = 
       doc.createElement("Properties");
087  eBean.appendChild(eProperties);
088 
089  // For each property of the bean, get a
     // DocumentFragment that
090  // represents the individual property.
     // Append that DocumentFragment
091  // to the Properties element of the document
092  for (int i = 0; i < pds.length; i++) {
093    PropertyDescriptor pd = pds[i];
094    DocumentFragment df = getAsDOM(
doc, bean, pd);
095    if (df != null) {
096 	 // Create a Property element and add to
         // Properties element
097 	 Element eProperty =
           doc.createElement("Property");
098      eProperties.appendChild(eProperty);
099 
100 	// Create NAME attribute, add it to
        //Property element,
101 	// and set it to name of property
102 	eProperty.setAttribute("NAME",
          pd.getName());
103 
104     // Append the DocumentFragment to the
        //Property element
105 	// This "splices" the entire
        // DOM representation of the
106 	// Property into the tree at this point.
107 	eProperty.appendChild(df);
108 	  }
109     }
110   }
111  return dfResult;
112  }


Listing 7. getAsDOM() encodes a bean as a DOM tree

The bean version of getAsDOM() encodes an entire JavaBean as a DOM DocumentFragment.

Lines Description
33-69 getAsDOM() first gets the class of the bean and determines if that class defines a custom method that encodes the bean as a DOM tree. It looks for a method whose name is getAsDOM, unless the class defines a method String getDOMGetterName(), in which case this method looks for a method of that name. (Please don't confuse the method we're going over here, XMLBeanWriter.getAsDOM(), and the method the bean class defines to encode itself as a DOM tree.) If a DOM getter method is found, it is called, and whatever it returns is the return value for this bean. This allows a bean programmer to take complete control over how the bean class is represented as a DOM tree (and, therefore, as an XML document).
 
74-110 If the bean doesn't know how to encode itself as a DOM tree, getAsDOM encodes the bean by creating an Element that represents the bean, and adding subnodes that represent the bean's properties. Getting the bean's properties as DOM trees involves reading all of the bean's properties, encoding each property as a DOM subtree (represented by a DocumentFragment), and then inserting the property's subtree in the list of properties for the bean. Line 72 gets the BeanInfo object for the JavaBean from the Introspector, and line 79 gets the list of PropertyDescriptors that describe the bean's properties. Lines 83 through 87 create the top node of the result DOM tree. Lines 92 through 110 loop through the bean's PropertyDescriptors, creating an Element for each property, encoding the property of the JavaBean as a subtree (by calling the second form of getAsDOM(), see below), and inserting the new Element in the <Properties> section of the result tree. When all properties have been encoded and added to the tree, the result tree returns to the caller in the form of a DocumentFragment object (line 111).

That's all the bean form of getAsDOM() does. The form of getAsDOM() that encodes properties appears in Listing 8 below.

113 /**
114  * Return a DOM DocumentFragment representing a
     * property of a JavaBean
115  * @return org.w3c.dom.DocumentFragment
116  * @param doc The document to use as a 
factory for
117  * subelements of the tree.
118  * @param bean The object that is the value of 
     * the property.
119  * This object must be a JavaBean.
120  * @param pd A property descriptor describing
121  * the property of which the object is a value.
122  * @exception IllegalAccessException
123  * @exception InstantiationException
124  * @exception IntrospectionException
125  */
126  public static DocumentFragment getAsDOM(
     Document doc, Object bean, 
     PropertyDescriptor pd) 
     throws IntrospectionException, 
     InstantiationException, 
     IllegalAccessException {
127 	Class classOfBean = bean.getClass();
128 	Class classOfProperty =
 pd.getPropertyType();
129 	DocumentFragment dfResult = null;
130 	String sValueAsText = null;
131 	Class[] paramsNone = {};
132 	Object[] argsNone = {};
133 
134 	// If the property is "class," and
        // the type is java.lang.class, then
135 	// this is the class of the bean, which 
        // we've already encoded.
136 	// So, in this special case, return null.
137 	if (pd.getName().equals("class") 
          && classOfProperty.equals(
          java.lang.Class.class)) {
138 		return null;
139 	}
140 
141  // 0. If pd is an XML property descriptor, and
     // we can call its
142  // getter method, do so and return. If the
     // programmer
143  // specifies a DOM getter method, we return
     // what it returns, no questions asked.
144  if (pd instanceof XMLPropertyDescriptor) {
145    Method getter;
146    if ((getter = ((XMLPropertyDescriptor) 
       pd).getDOMReadMethod()) != null) {
147 	 try {
148 	   Class[] params = 
             {org.w3c.dom.Document.class};
149 	   Object[] args = {doc};
150 	   dfResult = (DocumentFragment) 
             (getter.invoke(bean, args));
151 	 } catch (Exception ee) {
152 	     ; // Ignore... couldn't get the method
153 	   }
154 	 return dfResult;
155 	}
156  }
157 
158  // 1. Try to represent the property as XML.
159  // This bean may know how to describe itself,
     // or parts of
160  // itself, as XML. There are two possibilities:
161  // [a] The bean has a method called 
     // get<Propname>AsDOM()
162  // [b] The property class has a method called 
     // getAsDOM()
163  // We'll try both of these, and the first 
     // (if any) that
164  // works will be the DocumentFragment we want
     // to return.
165  // If none of these are true, then we try to
     // find the object's
166  // value as text
167 
168  // [1a] Does the bean have a method called
     // get<Propname>AsDOM()?
169  // Capitalize property name
170  StringBuffer sPropname = 
       new StringBuffer(pd.getName());
171  char c = sPropname.charAt(0);
172  if (c >= 'a' && c <= 'z') {
173 		c += 'A' - 'a';
174   }
175  sPropname.setCharAt(0, c);
176  String sXMLGetterName = "get" 
       + sPropname + "AsDOM";
177 
178  // If both of these methods succeed, then
     // dfResult will be set
179  // to non-null; that is, the method existed
     // and returned a
180  // DocumentFragment
181  try {
182    Class[] params = 
{org.w3c.dom.Document.class};
183    Method mXMLGetter = 
        classOfBean.getMethod(
        sXMLGetterName, params);
184 	Object[] args = {doc};
185 	dfResult = (DocumentFragment) 
          (mXMLGetter.invoke(bean, args));
186 	} catch (Exception ee) {
187 	   ; // Ignore... couldn't get the method
188 	}
189 
190  // Hereafter, we're trying to create a 
     // representation of the property
191  // based somehow on the property's value.
192  // The very first thing we need to do is get 
     // the value of the
193  // property as an object. If we can't do that,
     // we can get no
194  // representation of the property at all.
195  Object oPropertyValue = null;
196  try {
197    Method getter = pd.getReadMethod();
198    if (getter != null) {
199      oPropertyValue = 
           getter.invoke(bean, argsNone);
200 		}
201 	} catch (InvocationTargetException ex) {
202 	   ; // Couldn't get value. Probably 
             // should be an error.
203  }
204 
205  // [1b] If we don't have a DocumentFragment,
     // the previous block failed.
206  // So, let's find out if the property's 
     // class has a method called
207  // getAsDOM() and, if it does, 
call that instead.
208  if (dfResult == null) {
209    try {
210     Class[] params = 
{org.w3c.dom.Document.class};
211     Method mXMLGetter = 
          classOfProperty.getMethod("
          getAsDOM", params);
212     Object[] args = {doc};
213     dfResult = (DocumentFragment) 
        (mXMLGetter.invoke(oPropertyValue, args));
214    } catch (Exception ee) {
215 	   ; // Ignore—who cares why it failed?
216 	 }
217  }
218 
219 
220  // 2. Try to represent the property as a String.
221  // See if this property's value
222  // is something we can represent as Text, or if
     // it's something
223  // that must be represented as a JavaBean. 
     // Let's assume that this
224  // object can be represented as text if:
225  // [a] it has a PropertyEditor associated with
     // it, because
226  // PropertyEditors always have setAsText() and
     // getAsText()
227  // If it can't be represented as text, then 
     // we pass it to
228  // getAsDOM(Document, Object) and return 
     // the result.
229 
230  if (dfResult == null) {
231 
232    // [2a] Can we get either a custom or 
       // built-in property editor?
233    // If the PropertyDescriptor returns an 
       // editor class, we
234    // create an instance of it; otherwise, 
       // we ask the system for
235    // a default editor for that class.
236    Class pedClass = pd.getPropertyEditorClass();
237    PropertyEditor propEditor = null;
238    if (pedClass != null) {
239       propEditor = (PropertyEditor) 
            (pedClass.newInstance());
240     } else {
241 	propEditor = 
          PropertyEditorManager.findEditor(
            classOfProperty);
242 	}
243 
244   // If the property editor's not null, pass
      // the property's
245   // value to the PropertyEditor, and then ask 
      // the PropertyEditor
246   // for a text representation of the object.
247   if (propEditor != null) {
248     propEditor.setValue(oPropertyValue);
249 	sValueAsText = propEditor.getAsText();
250   }
251 
252   // If somewhere above we found a string value,
      // then create
253   // a DocumentFragment to return, and append 
      // to it
254   // a Text element.
255   if (sValueAsText != null) {
256     dfResult = doc.createDocumentFragment();
257 	Text textValue = doc.createTextNode(
sValueAsText);
258 	dfResult.appendChild(textValue);
259   }
260 }
261 
262  // 3. Try to represent the property value as a
     // JavaBean
263  // If we don't have a DocumentFragment yet,
we'll
264  // have to introspect the value of the object,
 because
265  // it's apparently something that can't be
     // represented
266  // as flat text. We'll assume it's a JavaBean.
267  // If it isn't... oh, well.
268  //
269  if (dfResult == null && oPropertyValue !=
 null) {
270    dfResult = getAsDOM(doc, oPropertyValue);
271 	}
272 	return dfResult;
273 }


Listing 8. getAsDOM() encodes a property as a DOM tree

Lines Description
127-128 The method first gets the class of the bean, and the class of the property within the bean.
137-139 Sometimes, the Class class shows up in a list of properties, in which case it is ignored.
141-156 If the PropertyDescriptor is also an XMLPropertyDescriptor, we use the DOM read method indicated by the XMLPropertyDescriptor to encode the property as a DOM tree.
158-188 If the bean class defines a method called get<i>Propertyname</i>AsDOM(), that method is used to retrieve the property as a DOM tree.
190-203 From line 190 on, the only way we can get a DOM representation of a property is by getting the property's value, formatting that value as a string, and embedding that string within a <Property> element. Lines 190 through 203 retrieve the value of the property by calling the traditional property-getter method indicated by the PropertyDescriptor.
205-217 If the class of the property has a method called getAsDOM(), we call that method on the value we just retrieved, and the result will be the property encoded as a DOM tree. Note that this is different from asking the bean class to encode the property as a DOM tree.
220-260 If we still haven't succeeded in creating a DocumentFragment to return, then we have to create the <Property> node and insert into it the property value encoded as a String. We use the PropertyEditor for the property type to perform the conversion.
262-271 If all else fails, we use the first version of getAsDOM() to encode the property as a JavaBean. Note that this means that the two versions of getAsDOM() are mutually recursive. At the end of this section of code, the resulting DocumentFragment representing the property is returned to the caller.

Other XMLBeans Classes

These classes in the XMLBeans package are for use by programmers who are adding XMLBeans functionality to their classes.

Class XMLSimpleBeanInfo
Class XMLSimpleBeanInfo exists only to fix a bug in IBM's implementation of java.beans.SimpleBeanInfo (which happens to be what I use to develop the code for this column). XMLSimpleBeanInfo could conceivably be extended to report more information about an XMLBean to the XMLBeans package, but to do so would require redevelopment (or at least subclassing) of the java.beans.Introspector, which would be too large a topic for this article.

Class XMLPropertyDescriptor
Class XMLPropertyDescriptor, an XML property descriptor, extends java.beans.PropertyDescriptor by allowing a programmer to specify the DOM setter and DOM getter for each property. Just as with traditional PropertyDescriptor objects, the methods may be specified by name, or as Method objects. XMLBeans searches for DOM accessors, and uses them preferentially over traditional accessors, thereby providing the XMLBeans customization described above. This class could probably be made smarter by having it introspect the bean class, looking for getAsDOM(), and so on, instead of keeping that logic in the XMLBeanReader and XMLBeanWriter classes; but again, that would require subclassing the Introspector.

Class Util
Class Util contains a hodgepodge of simple classes to ease coding. None are complicated enough to warrant description here.

Further work

XMLBeanReader and XMLBeanWriter are simply a starting point—many additional possibilities exist for experimentation beyond what you've read here.

As I mention in the code discussion, this package doesn't handle indexed properties in a general way, although specific indexed properties can be supported if coded by hand, as the Grades example demonstrates.

There are some minor things that need to be added to this package in order to make it real. In particular, the XMLBeans package doesn't encode the less than, greater than, and ampersand characters as it should. Each time a Text element is created, the data added to the Text element should first replace every greater than (>) with the sequence &gt;, every less than (<) with &lt;, and every ampersand with &amp;. This translation needs to be done in reverse when reading data from Text elements. These encodings are hard-and-fast requirements of XML, and are a major weakness of this package as currently implemented.

Another (perhaps less minor) necessary modification is a change in the code to handle CDATA sections, which are a special way of including large blocks of preformatted text in an XML file. CDATA sections should be allowed anywhere a Text element is allowed. (See the XML recommendation in the Resources section of this article for a description of CDATA sections.)

Still another way to modify this package is to improve the exception handling. I've been positively shameless about ignoring exceptions. My excuse is that this code is meant for demonstration purposes, and not to be used in a production environment.

A final way to improve the package would be to modify the XMLBeanWriter package to create XMLBeans files using the new XMLBeans format, instead of using the old format.

Some readers have written to ask me how to write XML that represents directed cyclic graphs without producing files of infinite length. The solution is quite simple. If you're interested in this, read up on IDREF in the XML recommendation and go from there. Each node in your source graph can be uniquely identified by an ID, and the nodes can then reference each other by IDREF.

While we've only output the properties of a JavaBean, there's no reason why you couldn't write code to dump the event connections of a bean as well. (For an example, see IBM's BML project in Resources.)

Some readers have written to me, excited about replacing Java standard serialization and versioning with XML. I think this is an excellent idea, and, if implemented properly, could easily be superior to the standard serialization mechanism.

This implementation of XML JavaBeans actually gives up a little too soon when it tries to convert an object to a string. The final thing the XMLBeanWriter should do is check to see if the object implements Serializable. If it does, XMLBeanWriter could serialize the object to a byte array, and then encode the array as a BCD or base64 string. The reverse operation would have to be performed when reading the JavaBean from XML.

I hope you've found the XMLBeans series informative and interesting. I believe XML has the potential to be more revolutionary than even the World Wide Web. XML will affect the Web immensely, but will also appear in systems that never see a network. Be watching for a synthesis of XML and component technology; it's a combination with a bright future.

RESOURCES:

Download the source code for this article
In jar format (with class files): http://www.javaworld.com/jw-07-1999/beans/XMLBeans.jar
In gzipped tar format: http://www.javaworld.com/jw-07-1999/beans/XMLBeans.tar.gz
In zip format: http://www.javaworld.com/jw-07-1999/beans/XMLBeans.zip
The first two articles in the XML JavaBeans series
"XML JavaBeans, Part 1," Mark Johnson (JavaWorld February 1999)
"XML JavaBeans, Part 2," Mark Johnson (JavaWorld March 1999)
More XML background articles and documentation
"XML for the absolute beginner" Mark Johnson (JavaWorld April 1999). A gentle introduction to XML
"Soup up your Java classes with customization," Mark Johnson (JavaWorld December 1998)
"A fistful of values," Mark Johnson (JavaWorld November 1998). Although we didn't cover indexed properties in this column, you may want to experiment with XML JavaBeans by adding indexed property support. The November 1998 JavaBeans column is a good place to start understanding indexed property editors. A JavaWorld exclusive, the article is one of the few resources available anywhere that explains how to write an indexed property editor
"The trick to controlling bean customization," Mark Johnson(JavaWorld November 1997). An essential background article on properties, property editors, property descriptors, BeanInfo, and customization
W3C recommendation for XML. A not-so-gentle, yet still quite readable, introduction to XML
IBM XML resources
The parser from IBM's XML4J package is available free for noncommercial use. It's even free for commercial use, but be sure to read the license agreement IBM's Bean Markup Language (BML), another example of encoding JavaBeans in XML

Reprinted with permission from the June 1999 edition of JavaWorld magazine. Copyright Web Publishing Inc., an IDG Communications company. Register for editorial e-mail alerts at: http://www.javaworld.com/javaworld/common/jw-subscribe.html?JDevCon

coffeecup

About the Author

Mark Johnson has a BS in Computer and Electrical Engineering from Purdue University (1986), and has been writing for JavaWorld since August of 1997. By day, he works as a designer and developer for Object Products, Inc., in Fort Collins, CO.

[TOP]