Adopt Adapter.
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 Node
s) you want to display using Swing's JTree
. The JTree
class can't display your Node
s directly, but it can display TreeNode
instances. With an adapter, you can map your Node
s to TreeNode
s. 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 Adaptee
s) for target (Target
) objects. For the JTree
in Figure 1, I adapt the objects in the tree (UIComponent
s) 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
.
You can easily adapt existing tree structures to JTree
s 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 TreeNode
s.
Examine Figure 4 again. Notice that TreeModel
s deal exclusively in Object
s, but DefaultTreeModel
deals in TreeNode
s. 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 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 (JComponent
s) and Swing tree nodes (DefaultMutableTreeNode
s). 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.
Predictably, SwingComponentNode
conforms to Figure 2's adapter; it recasts JComponent
s as ExplorableTreeNode
s 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 explorerThe 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.
You use file node adapters just like Swing component nodes. Example 4 shows how you can use FileNode
s.
Source: www.bing.com
Images credited to www.bing.com and 0f469d6f2fa468202d31-6b0d87410f7cc1525cc32b79408788 c4.ssl.cf2.rackcdn.com