Process XML with JavaBeans, Part 2

How IDEs interconnect components

In last month's column, I discussed the creation of XML-processing applications that have little or no user interface. That article covered the DOMGenerator, XMLTokenizer, XMLFileGenerator, and XMLStringGenerator components, all of which are nonvisual beans.

Last month's components showed you how to read and write XML trees. This month, I'll show you how to use the nonvisual XMLEditor bean set to process those trees. This article will be highly technical, so you must have prior knowledge of how JavaBeans interconnect in an integrated development environment (IDE) at the lowest level. I'll give a detailed example of a very simple application, with explanations of event and property connections, and describe how you can use them to create applications without programming.

This article assumes you've read Part 1 of this series and that you're familiar with the basics of XML, the Document Object Model (DOM), and JavaBeans, as well as the concept of building applications in an IDE. It would also be helpful to download and install IBM's XML Beans from IBM's alphaBeans site (see Resources).

The XMLEditor bean set

XMLEditor beans do not necessarily edit XML, despite what their name implies. Instead, these beans form a hierarchy of containers and operators that match particular features of an org.w3c.dom.Document or a org.w3c.dom.Node (which I will simply call Document, Node, Element, etc.), and hold a reference to that object for processing.

That object represents a part of the input XML tree being edited. As shown last month, a DOMGenerator bean creates a Document object, representing the tree of XML nodes. But that capability is almost useless if you can't pick the tree apart and use its pieces. That's where XMLEditor beans come in.

I'll start with an example that clarifies what I'm discussing here. Part of the sample XML I've been using for these articles, a recipe for Lime Jello Marshmallow Cottage Cheese Surprise, appears in Figure 1 in two different forms.

Figure 1. Recipe XML as text (left) and as objects (right)

The left side of the figure represents part of the sample XML document, encoded as ASCII text. (You can find a link to the entire sample XML code in the Resources section below.) On the right is the same document, represented as a tree of DOM Node objects (subclasses of org.w3c.dom.Node). The DOMGenerator (by way of its underlying parser) produces this tree from the input XML stream.

The top-level element is the Document node, only one of which exists for any given XML document. The DOMGenerator object creates an object of this type and provides it as its result property. A Document is a container that, if not empty, holds one and only one object of type Element, which is in turn given the same name as the Document's top-level tag. This element is called the document element. In this example, the document element is <Recipe>. (With the notation <Tagname>, I indicate both the name of an XML tag and the tag name returned by the getTagName() method of a corresponding DOM Element object.)

The <Recipe> contains a single <Name> that contains a Text item. The Text item's value is the name of the recipe. This structure is defined and enforced by the Document Type Definition (DTD) for the XML document. (You can see the simple DTD for this Recipe XML in Resources below.)

What does all this have to do with XMLEditor beans? Each container bean class in the XMLEditor bean set recognizes a particular Node type at the bean class's input. For example, a DocumentContainer XMLEditor bean takes a Document node as its input. A DocumentContainer's input is the value of its inputDocument property, which is write only. The DocumentContainer contains a read-write property called currentNode, which is the document to be edited. The DocumentContainer has a third property, result, which is read only, and returns the Document object that the DocumentContainer currently contains.

The inputDocument (write only) and result properties (read only) can be different because the document within the DocumentContainer (the currentNode) is editable. After editing (setting the currentNode property to refer to a different Document object, for example), the result document -- that is, the output -- may well be different from the input.

Let's apply this to the example above. Imagine setting a DocumentContainer's inputDocument to the Document object at the top of the object tree in Figure 1. The DocumentContainer will make the Document available as a Node (since a Document is a subclass of Node, and therefore is a Node), and then make that Node available as its currentNode property. It will also make the Document available as its result property.

To understand how an application composed of interconnected objects works, you must understand how to interconnect objects. What does it mean for objects to be connected to one another?

Object connection: An in-depth example

Returning to the example in Figure 1, let's build a small application that gives the user a view of a recipe's name -- that is, the contents of the

<Name>

element inside a

Document

's

<Recipe>

element. Figure 2 shows the configuration of just such an application running inside the IBM VisualAge for Java IDE. Other IDEs may present the connections between beans in completely different ways, but all serious IDEs allow connections between bean properties, events, and methods.

Figure 2. Configuration of the RecipeEditor application

The purpose of the RecipeEditor bean is simply to extract the text of the <Name> element of a Document containing a <Recipe> and display that <Name> to the user.

For the rest of this section, I'm going to delve into the inner workings of how IDEs connect objects to one another, using the simple RecipeEditor as an example. If you already understand event listener relationships and the ways in which IDEs interconnect components to create applications, you can skip down to the section titled An unprogrammed application .

Clicking the Open File button causes the DOMGenerator object (marked A in Figure 2) to read its input, parse any XML it finds, and produce a DOM Document as its output. But how exactly does clicking the button cause this to happen?

Making the connection

The arrow in Figure 2 between the Open File button and

DOMGenerator1

indicates an event listener relationship. In this case, the

DOMGenerator

's

triggerAction()

method is called when the Open File button produces an

ActionEvent

. Calling

triggerAction()

causes the

DOMGenerator

to fetch its input document, parse the document, and produce a

Document

object as its result.

It's fairly easy to create a DOMGenerator that listens for ActionEvents on another object. For example:

public class DOMGeneratorThatListens
  extends DOMGenerator implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      triggerAction();           
    }
}

Then, elsewhere in the code:

Button openFile = new Button("Open file...");
DOMGeneratorThatListens dg = new DOMGeneratorThatListens();
openFile.addActionListener(dg);

Simple enough. In real applications, though, many objects listen for events from multiple sources, and they may also send events to other objects. The resulting code quickly becomes too complex for an IDE to handle easily.

Being adaptable

A common solution to this problem is to split the listening and reacting functionality out of the subclass and place it in a separate class, called an event adapter class. An event adapter is simply a dispatcher that listens for events on an event source and then calls methods on the event target. Code for an event adapter might look like this:

public class DOMGeneratorButtonAdapter implements ActionListener {
   DOMGenerator dg = null;
   Button b = null;
   DOMGeneratorButtonAdapter(Button source, DOMGenerator target) {
      b = source;
      dg = target;
      b.addPropertyChangeListener(this);
   }
   public void actionPerformed(ActionEvent e) {
      dg.triggerAction();
   }
}

You could then link together a button and a DOMGenerator without subclassing either of them, like this:

Button openFile = new Button("Open file...");
DOMGenerator dg = new DOMGenerator();
DOMGeneratorButtonAdapter dgba = new DOMGeneratorButtonAdapter(openFile, dg);

When the button is clicked, the adapter receives the event and then performs the method call on the target object.

A purely object-oriented solution would be to create a separate event adapter object for each connection. Some IDEs may do so, but the designers of VisualAge for Java decided to turn the application class (in this case, RecipeEditor) into the event adapter for all events. In other words, the application class listens for all events from objects (e.g., clicks on the Open File button) and then dispatches calls to the appropriate targets (e.g., the DOMGenerator's triggerAction()).

When the user clicks the Open File button, the button sends an ActionEvent to all of its listeners by passing it to those listeners' actionPerformed() methods. The application class (RecipeEditor) is registered as a listener for ActionEvents, and it responds to the button's ActionEvent by calling the DOMGenerator's triggerAction() method.

How connections are created

In Figure 2, the application developer created the connection between the button and the DOMGenerator in the IDE. The arrow leading from the button to the DOMGenerator is a graphical representation of a method that the IDE generated automatically. The application developer indicated (graphically, using mouse clicks and menus) that the IDE should connect the button's actionPerformed() method to the DOMGenerator's triggerAction() method. The IDE then automatically took the following steps:

  1. It declared the application class RecipeEditor as an ActionListener by adding implements ActionListener to the RecipeEditor class declaration. This made the class capable of receiving ActionEvent objects.

  2. It wrote a handler method actionPerformed, which detects whether the source of the ActionEvent was the Open File button and, if so, calls triggerAction() on the DOMGenerator. Here's a sample of the code the IDE produced:

    /* WARNING: THIS METHOD WILL BE REGENERATED. */
    public void actionPerformed(ActionEvent e) {
        if ((e.getSource() == getButton3()) ) {
            connEtoM11(e);
        }
        if ((e.getSource() == getOpenFile()) ) {
            connEtoM1(e);
        }
    }
    /* WARNING: THIS METHOD WILL BE REGENERATED. */
    private void connEtoM1(ActionEvent arg1) {
        getDOMGenerator().triggerAction();
    }
    

    You can see that the IDE wrote two methods: actionPerformed(), which checks the source of the event, and connEtoM1(), which actionPerformed calls after making that check and which actually performs the connection. Notice also that, instead of using the user interface element objects directly, the IDE has created accessors for them (getOpenFile() and getDOMGenerator()). This is good programming style, as it encapsulates access to the class's internal objects.

  3. Finally, the IDE registered the application class as a listener for ActionEvents on the Open File button in the RecipeEditor.initConnections() method, called from the constructor. initConnections() is a method, written entirely by VisualAge for Java, that is responsible for creating property change and event relationships.

Code generated by other IDEs would look very different from code that appears in the source files, but it would do essentially the same thing: it would call triggerAction() when the Open File button is pushed. (You can find RecipeEditor.java in the source archive, downloadable in jar, tgz or zip formats from Resources below.)

This is the standard way in which IDEs assemble objects into applications: they create instances of component classes and interconnect them with event listener and property change listener relationships. Each object maintains a list of other objects to notify when particular events occur. When an event occurs on an object (a mouse click, a query result return, a method call, input ready to be read, and so forth), the object generates an event (itself an object) describing what happened, and passes it to all the other objects listening for such events. That way, objects can be assembled into applications without compiling anything at runtime.

A component knows what other objects wish to be notified about an event because, earlier in the program's run, the object was added to the component's list of listeners for that event by way of a call to the component's addeventTypeListener() method.

1 2 Page 1