|
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 Background readingThis 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 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 XMLBeansI 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 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 ( 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:
Likewise, the process for writing a JavaBean is straightforward:
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 improvementcustomizationis next. CustomizationSince 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
The method
As noted above, if a JavaBean contains a method called
So, for example, imagine we had a bean class called
Furthermore, imagine that the
Using
Of course, the new version of the XMLBeans package removes this limitation, by
allowing specification of a
To provide more flexibility, an XMLBeans programmer may define
You may remember from Part 2 that it wasn't easy to prevent
XML property descriptors and property editors
Typically, a JavaBean designer controls the visibility of bean properties to
external agents by creating a
For example, imagine a JavaBean
A
If you think about it for a moment, it should be pretty clear to you that the
In addition, we can create a new subclass of
By creating the new
The second way that the new XMLBeans package integrates with
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
Of course we aren't. If a 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 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.")
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
So, for example, let's say we had a JavaBean class called
In our original XML dialect, using the DTD in Listing 1, the first few lines of an XMLBean might look something like Listing 2:
Listing 2. Part of a sample Player bean encoded in JavaBean XMLEncoding the same information in the new format is much clearer, as in Listing 3:
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 Listing 3 exemplifies all of the following rules for the new syntax except for the fifth:
A couple of minor details about these new syntax rules bear discussion. First,
why did we maintain the
The
I've described the improvements to 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
In addition, there are sample XML files,
Class Player
The properties
Class Statistics
Class PersonName
Class Grades
Class PlayerBeanInfo
Class StatisticsBeanInfo 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
Let's look at
readXMLBean()
Listing 4.
| ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
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 }
|
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
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 exceptionsthis
// 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 }
|
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 ; // Ignorewho 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 }
|
| 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.
|
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.
XMLBeanReader and XMLBeanWriter are simply a starting
pointmany 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 >, every less than (<) with
<, and every ampersand with &. 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.
BeanInfo, and customization
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
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]
|
| ||||||||||||