Sep 26, 2003 1:00 AM PT

Adopt Adapter

Understand how adapters let disparate systems work together

Pretend you're back in 1999 and you've just landed a job with a dot-com. Much of your compensation comes from stock options, and you are content. Fortunately, your work is interesting. You're developing an application builder that lets users visually construct Swing applications. So you seize upon a novel idea: display a view's component hierarchy next to the view itself like this:

Figure 1. A user interface (UI) builder prototype. Click on thumbnail to view full-size image.

The left panel shows the component hierarchy for the upper-right panel, and the lower-right panel contains a button that updates the tree. The idea, of course, is that users drag components into the upper-right panel and subsequently click the show component tree button to update the tree. In this simple prototype, you're only interested in getting the tree to reflect the upper-right panel's component hierarchy; you'll leave the drag and drop to someone else.

You propose this idea of exposing component trees to your colleagues, and they are enthusiastic. How long, they wonder, will it take to implement? The response is up to you, but as we're about to find out, the Adapter design pattern makes this an easy job.

Note: You can download this article's source code from Resources.

Introducing Adapter

Adapters are necessary because dissimilar elements need to interoperate. From wrenches to computer networks, physical adapters are abundant. In software, adapters make dissimilar software packages work together; for example, you might have a tree of objects (call them Nodes) you want to display using Swing's JTree. The JTree class can't display your Nodes directly, but it can display TreeNode instances. With an adapter, you can map your Nodes to TreeNodes. Because Swing trees use the Adapter pattern, you can display any kind of tree—from Document Object Model (DOM) to Swing component hierarchies to a compiler parse tree—just by implementing a simple adapter.

In Design Patterns, the authors describe the Adapter pattern like this:

Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces.

Figures 2 and 3 show the Adapter pattern's two standard variations.

Figure 2. Adapter with inheritance. Click on thumbnail to view full-size image.
Figure 3. Adapter with delegation. Click on thumbnail to view full-size image.

Adapters masquerade as one type of object by implementing its interface; they inherit (or delegate) functionality from another class. That way, you can effectively substitute objects (known as Adaptees) for target (Target) objects. For the JTree in Figure 1, I adapt the objects in the tree (UIComponents) to Swing, so a JTree instance can manipulate them. Considering the amount of code that sits behind JTree, this simple adapter provides a huge return on investment. Let's see how it works.

JTree crash course

First you need a fundamental understanding of Swing trees. So before I discuss Figure 1's adapter, I'll provide an overview.

The JTree class encompasses all the moving parts that make up Swing trees. Figure 4 displays a partial class diagram for JTree.

Figure 4. JTree class diagram. Click on thumbnail to view full-size image.

You can easily adapt existing tree structures to JTrees because, like all Swing components, JTree is built on a Model-View-Controller (MVC) architecture. That architecture separates the tree's data from the methods that manipulate and display the data. As Figure 4's class diagram shows, JTree instances access tree data through a TreeModel interface.

To adapt an existing tree structure to a JTree, you can implement the TreeModel interface and pass an instance of that model to JTree.setModel() like this:

public class MyTreeModel implements javax.swing.tree.TreeModel {
   // Map whatever tree structure you want to the TreeModel interface.
}
...
JTree tree = new JTree(new MyTreeModel()); // Create a tree with your new model.

If you look at Figure 4's TreeModel, implementing that interface seems easy. It is for static trees; however, if you want dynamic trees you must support tree model listeners and fire events at the proper time when the model changes. Neither of those tasks are trivial, so Swing provides a default tree model—DefaultTreeModel—that takes care of that bookkeeping by maintaining a tree of TreeNodes.

Examine Figure 4 again. Notice that TreeModels deal exclusively in Objects, but DefaultTreeModel deals in TreeNodes. This means that instead of implementing TreeModel directly, you can extend DefaultTreeModel and implement a tree node adapter like this:

public class MyTreeNode extends DefaultMutableTreeNode {
   // Map whatever tree strucure you want to the TreeModel interface
}
...
JTree tree = new JTree(new DefaultTreeModel(new MyTreeNode()));

Tree node adapters typically extend DefaultMutableTreeNode because that class implements the grunt work of adding and removing children from a node (methods specified by the MutableTreeNode interface). An instance of that tree node adapter is installed as the root node for the default tree model. Now you just need to implement MyTreeNode.

The rest of this article discusses the implementation of two tree node adapters, similar to MyTreeNode discussed above: one for Swing components and another for a file browser.

The UI builder

Figure 1's user interface (UI) builder prototype is shown once again in Figure 5 for a single panel containing five buttons. The buttons are laid out by BorderLayout.

Figure 5. UI builder at startup. Click on thumbnail to view full-size image.
Figure 6. The builder reveals the component tree. Click on thumbnail to view full-size image.

Figure 5 shows the application at startup; Figure 6 shows the application after a user clicks on the show component tree button. This application's main attraction is the SwingComponentNode class—an adapter that lets Swing components masquerade as tree nodes. That class is listed in Example 1.

Example 1. SwingComponentNode adapter

import java.awt.Component;
import java.awt.Container;
import javax.swing.JComponent;
import javax.swing.tree.*;
class SwingComponentNode extends DefaultMutableTreeNode 
                         implements ExplorableTreeNode {
   private boolean explored = false;
   public SwingComponentNode(JComponent swingComponent) { 
      setUserObject(swingComponent); 
   }
   public boolean getAllowsChildren() { return isContainer(); }
   public boolean isLeaf()            { return !isContainer(); }
   public JComponent getComponent()   { return (JComponent)getUserObject(); }
   public boolean isContainer() {
      return getComponent().getComponentCount() > 0;
   }
   public String toString() {
      return getComponent().toString();
   }
   public boolean isExplored() { 
       return explored; 
   }
   public void explore() {
      if(!isContainer())
         return;
      if(!isExplored()) {
         Component[] children = getComponent().getComponents();
         for(int i=0; i < children.length; ++i) 
            add(new SwingComponentNode((JComponent)children[i]));
         explored = true;
      }
   }
}

Notice the mapping between Swing components (JComponents) and Swing tree nodes (DefaultMutableTreeNodes). A Swing component has no notion of whether it is a leaf, so JComponent doesn't have an isLeaf() method like DefaultMutableTreeNode. As a result, the SwingComponentNode.isLeaf() method uses JComponent.getComponentCount() to determine if a Swing component is a leaf.

The SwingComponentNode class also implements an ExplorableTreeNode interface that I defined with the two methods expore() and isExplored(). The explore() method is called by a tree expansion listener and adds instances of SwingComponentNode as children of the node being expanded.

Figure 7 displays the SwingComponentNode class diagram.

Figure 7. A component node adapter. Click on thumbnail to view full-size image.

Predictably, SwingComponentNode conforms to Figure 2's adapter; it recasts JComponents as ExplorableTreeNodes by delegating to a JComponent instance. SwingComponentNode employs the delegation variation of the Adapter pattern, as shown in Figure 3. Example 2 shows how you use the adapter.

Example 2. Use the SwingComponentNode adapter

import java.io.File;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.tree.*;
abstract class TreePanel extends JPanel {
   abstract TreeModel createTreeModel();
   public void createTree() {
      final JTree tree = new JTree(createTreeModel());
      JScrollPane scrollPane = new JScrollPane(tree);
      setLayout(new BorderLayout());
      add(scrollPane, BorderLayout.CENTER);
      tree.addTreeExpansionListener(new TreeExpansionListener() {
         public void treeCollapsed(TreeExpansionEvent e) {
            // Don't care about collapse events
         }
         public void treeExpanded(TreeExpansionEvent e) {
            ...
            TreePath path = e.getPath();
            ExplorableTreeNode node = (ExplorableTreeNode)
                           path.getLastPathComponent();
            if( ! node.isExplored()) {
               DefaultTreeModel model = (DefaultTreeModel)tree.getModel();
               ...   
               node.explore();
               model.nodeStructureChanged(node);
            }
         }
         ...
      });
   }
}
public class Test extends JFrame {
   ...
   public Test() {
      ...
      updateButton.addActionListener(new ActionListener() {
         private boolean treeCreated = false;
         public void actionPerformed(ActionEvent event) {
            if(!treeCreated) {
               TreePanel treePanel = new TreePanel() {
                  public TreeModel createTreeModel() {
                     SwingComponentNode rootNode = new SwingComponentNode(upperRightPanel);
                     rootNode.explore();
                      return new DefaultTreeModel(rootNode);
                  }
               };
               ...
            }
         }
      });
   }
   public static void main(String args[]) {
      GJApp.launch(new Test(),"UI Builder Prototype", 300,300,600,175);
   }
}

I implemented an abstract TreePanel class that creates a tree, puts it in a scrollpane, and handles tree node expansion events by calling the node's explore() method as needed. Concrete TreePanel extensions must implement createTreeModel(), which supplies the tree's model. The nodes in that model must implement the ExpandableTreeNode interface.

The Test class creates a TreePanel instance with an anonymous inner class that creates a default tree model with a SwingComponentNode for the root node. That node represents the upper-right panel in the application. From there, JTree takes care of the rest.

A file explorer

A true test of any Adapter implementation is how easily you can create and integrate a new adapter. So after I created the UI builder prototype discussed above, I decided to test the Swing Adapter implementation by creating an adapter for files. Figure 8 shows the result.

Figure 8. A file explorer

The preceding application explores a filesystem with a Swing tree. First, I implemented a new tree node adapter for files, which Example 3 lists.

Example 3. FileNode tree node adapter

import java.io.File;
import javax.swing.tree.*;
class FileNode extends DefaultMutableTreeNode 
               implements ExplorableTreeNode {
   private boolean explored = false;
   public FileNode(File file) { 
      setUserObject(file); 
   }
   public boolean getAllowsChildren() { return isDirectory(); }
   public boolean isLeaf()     { return !isDirectory(); }
   public File getFile()       { return (File)getUserObject(); }
   public boolean isDirectory() {
      File file = getFile();
      return file.isDirectory();
   }
   public String toString() {
      File file = (File)getUserObject();
      String filename = file.toString();
      int index = filename.lastIndexOf(File.separator);
      return (index != -1 && index != filename.length()-1) ?
                           filename.substring(index+1) : 
                           filename;
   }
   public boolean isExplored() { 
       return explored; 
   }
   public void explore() {
      if(!isDirectory())
         return;
      if(!isExplored()) {
         File file = getFile();
         File[] children = file.listFiles();
         for(int i=0; i < children.length; ++i) 
            add(new FileNode(children[i]));
         explored = true;
      }
   }
}

Notice the similarities between Example 1 and Example 3; once you have one adapter, it's easy to create new ones. Figure 9 shows the FileNode adapter's class diagram. Notice the similarities between Figure 9 and Figure 7.

Figure 9. A file node adapter. Click on thumbnail to view full-size image.

You use file node adapters just like Swing component nodes. Example 4 shows how you can use FileNodes.

Example 4. Use the FileNode adapter

import java.io.File;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.tree.*;
abstract class TreePanel extends JPanel {
   abstract TreeModel createTreeModel();
   public void createTree() {
      final JTree tree = new JTree(createTreeModel());
      JScrollPane scrollPane = new JScrollPane(tree);
      setLayout(new BorderLayout());
      add(scrollPane, BorderLayout.CENTER);
      tree.addTreeExpansionListener(new TreeExpansionListener() {
         public void treeCollapsed(TreeExpansionEvent e) {
            // Don't care about collapse events.
         }
         public void treeExpanded(TreeExpansionEvent e) {
            ...
            TreePath path = e.getPath();
            ExplorableTreeNode node = (ExplorableTreeNode)
                           path.getLastPathComponent();
            if( ! node.isExplored()) {
               DefaultTreeModel model = (DefaultTreeModel)tree.getModel();
               ...   
               node.explore();
               model.nodeStructureChanged(node);
            }
         }
         ...
      });
   }
}
public class Test extends JFrame {
   ...
   public Test() {
      ...
      updateButton.addActionListener(new ActionListener() {
         private boolean treeCreated = false;
         public void actionPerformed(ActionEvent event) {
            if(!treeCreated) {
               TreePanel treePanel = new TreePanel() {
                  public TreeModel createTreeModel() {
                     FileNode rootNode = new FileNode(new File("/"));
                     rootNode.explore();
                     return new DefaultTreeModel(rootNode);
                  }
               };
               ...
            }
         }
      });
   }
   public static void main(String args[]) {
      GJApp.launch(new Test(),"UI Builder Prototype", 300,300,600,175);
   }
}
// Class GJApp omitted for brevity.

The file adapter showcases the Adapter pattern's power. With very little effort, you can adapt any type of tree structure into objects JTree can manipulate.

Keep adapting

The Adapter pattern lets disparate object types work together. It allows Swing trees to manipulate and display any type of tree structure. The requisite adapters are simple and easy to plug into the tree's model.

David Geary is the author of Core JSTL Mastering the JSP Standard Tag Library (Prentice Hall, 2002; ISBN: 0131001531), Advanced JavaServer Pages (Prentice Hall, 2001; ISBN: 0130307041), and the Graphic Java series (Prentice Hall). David has been developing object-oriented software with numerous object-oriented languages for almost 20 years. Since reading the GOF Design Patterns book in 1994, David has been an active proponent of design patterns, and has used and implemented design patterns in Smalltalk, C++, and Java. In 1997, David began working full-time as an author and occasional speaker and consultant. David is a member of the expert groups defining the JSP Standard Tag Library and JavaServer Faces, and is a contributor to the Apache Struts JSP framework. David is currently working on Core JavaServer Faces, which will be published in the spring of 2004.

Learn more about this topic