Find out how to event-enable and network a Swing-based client Swing, the powerful GUI toolkit bundled with JDK 1.2, includes a lot of useful widgets, support for multiple look and feels, and a variety of other features you’ll want to exploit in your apps. In our last installment we used Swing to give our tired old Forum message client a more commercial-quality interface. This time we’re going to outfit the Swing Forum with the ability to read articles and threads from the Forum server, as well as post articles to it.We’ll start with the interface code we developed last time. To this we’ll add listeners to enable reading, replying, and posting functionality. We’re going to reuse a networking class from a previous installment to implement the networking to the Forum server, which we’ll also reuse from a previous installment.In response to a reader suggestion, we’re going to add a JSplitPane between the tree on the left and the tabbed display area on the right. On JTree and TreeModelsBefore diving into the new code, we should take a closer look at the interface component that will be the focus of most of our effort: the JTree.The JTree is the central widget in the Swing Forum. Users interact with the tree to select threads and the articles they contain. The tree reflects the server content (or a portion of it) at the point in time that the tree refreshes its contents from the server.The tree appearing in the interface represents an interaction of several types of objects: JTree handles the view of the tree dataDefaultTreeModel handles the data the view displaysDefaultMutableTreeNode encapsulates a node in the tree modelForumArticle encapsulates article contentsThe JTree displays data from the DefaultTreeModel, which is composed of DefaultMutableTreeNodes.The JTree itself handles the tasks of displaying the tree structure and handling user events. The contents of the tree are stored in a tree data model that implements the TreeModel interface; in this case, we use the supplied DefaultTreeModel. The tree model is made up of tree nodes, each of which is an instance of DefaultMutableTreeNode.Each DefaultMutableTreeNode instance contains a “user object” that represents user data at that node in the tree. In our case, the user object is either a String (the name of a thread) or an instance of ForumArticle (which encapsulates an article).To allow the user to manipulate the threads and articles on the server, we need to define user actions on the tree for refreshing and navigating. We’ll specify that double mouse clicks make refresh calls for threads and the root node (the Forum server node), and we’ll enable navigation of the tree with single mouse clicks and arrow keys.Communications and networkingBecause the server is the reference copy of articles and threads, we need to find some way to access the server and display its contents in the tree. Our old friend networking comes to the rescue here.We’ll enable networking with a sockets-based communications object that implements the 1.0 Forum API. The 1.0 Forum API has three methods, which are called by the Swing Forum event handlers based on user interaction with the GUI. The signatures for these methods are defined in the SwingForumClientCom interface. Hashtable loadAllThreads ()Vector loadThreadArticles (String thread)boolean postArticle (String art, String thread)Of course, any communications object that implements this interface will work in place of the sockets-based object we’re using; we designed the system way back when for just this sort of flexibility.For the purposes of the Swing Forum, we’ve modified an old ForumClientComSocketImpl to take a URL in its constructor. We won’t detail the code for this or the server portions of the article — they’re basically taken whole-cloth from past installments (see Resources for a listing of past Step by Step columns). The main difference is the constructor: public SwingForumClientComSocketImpl (URL u) { serverURL = u; } The SwingForumClientComSocketImpl implements the SwingForumClientCom interface, which enforces implementation of the Forum 1.0 API. Using 1.1 eventsThe Swing Forum uses the 1.1 event model to define event handling for the trees as well as the other elements in the user interface.Recall that the 1.1 event model is based on adding event listeners to objects that produce the events. For example, a JButton produces an ActionEvent when it’s clicked. The following statement adds an ActionListener to a JButton called jb: // jb is an instance of JButton jb.addActionListener (jbListener); jb‘s event code calls the actionPerformed(...) method of jbListener every time the button is clicked. But how do we get this instance of jbListener? We could go the standard route — define a new class that implements the ActionListener interface and then pass a new instance of this in the add call above. This works, but the standard approach doesn’t take advantage of a handy 1.1 shortcut — the anonymous class. This technique lets us do it all at once, plus define an inner class that has access to class variables and methods. jb.addActionListener (new ActionListener () { public void actionPerformed (ActionEvent e) { // do something here - enclosing scope enabled } }); We’ll make use of this technique extensively in the Swing Forum.Additions to the SwingForum classNow, time for the code. The following sections of code have been added to the SwingForum code we developed last time.import java.awt.*; import java.awt.event.*; import java.util.*; import java.net.*; import com.sun.java.swing.*; import com.sun.java.swing.text.*; import com.sun.java.swing.tree.*; import com.sun.java.swing.event.*; public class SwingForum extends JFrame { static final String QUOTE_CHARS = "> "; This section is almost identical to the original SwingForum. Along with several more import statements, the QUOTE_CHARS definition is added to specify the characters that are prepended to each line of existing text in a reply. replyAction = new AbstractAction ("Reply") { public void actionPerformed (ActionEvent e) { quoteReply (); display.setSelectedComponent (replyTab); } }; refreshAction = new AbstractAction ("Refresh Server") { public void actionPerformed (ActionEvent e) { loadAllThreads (); } }; These are the only two action definitions that are affected by the addition of the new functionality. When the reply action occurs, the replyArea is reset by the quoteReply() method to contain reply text quoted with the QUOTE_CHARS. When the refresh action occurs, the client rereads the threads from the server by calling the loadAllThreads() method. JTree tree; JScrollPane treeScroller; DefaultTreeModel treeModel; void layOutTree () { tree = new JTree (); tree.getSelectionModel ().setSelectionMode ( TreeSelectionModel.SINGLE_TREE_SELECTION); treeScroller = new JScrollPane (tree); loadAllThreads (); // load threads for root || arts for thread tree.addMouseListener (new MouseAdapter () { public synchronized void mouseClicked (MouseEvent e) { TreePath path = tree.getPathForLocation (e.getX (), e.getY ()); if (path != null) { DefaultMutableTreeNode node; node = (DefaultMutableTreeNode) path.getLastPathComponent (); if (e.getClickCount () == 2) { if (node.isRoot ()) { loadAllThreads (); tree.setSelectionPath (new TreePath (treeModel.getRoot ())); } else if (((DefaultMutableTreeNode)node.getParent()).isRoot()) { loadThreadArticles (node); tree.setSelectionPath (path); tree.expandPath (path); } } } } }); The layOutTree method is the first of two 1.1 event adapters that we define to handle populating the tree with threads and articles. This adapter listens to mouse clicks and gets a TreePath using the x and y position of the click. If the path isn’t null, the logic gets the last component of the path, which represents the node that has just been clicked.If the user double-clicks the mouse, the method will load either the threads from the server or the articles in a thread, depending on whether or not the double-click was on the root level or the second level of the tree. After layOutTree loads the threads or articles, it selects the item that was double-clicked and expands the path to it so that it’s visible in the display. // display art if selected. ignore root and thread selection tree.addTreeSelectionListener (new TreeSelectionListener () { public synchronized void valueChanged (TreeSelectionEvent e) { TreePath path = e.getPath (); DefaultMutableTreeNode node; node = (DefaultMutableTreeNode) path.getLastPathComponent (); if ((path.getPath ()).length > 2 ) { ForumArticle art = (ForumArticle) node.getUserObject (); readArea.setText (art.getText ()); display.setSelectedComponent (readTab); } else { readArea.setText (""); } } }); This listener listens to the tree for selection events. Selection events occur whenever a user keyboards to or clicks on a node, or when a node is selected programmatically.We’re interested only in hearing selections that occur to nodes representing articles. Remember, we decided that double-clicks on the server and threads would be required to cause refreshes; otherwise, we would have put the refresh-from-server code in here instead of in a mouse listener. Note that we could have used the mouse listener (rather than the one above) to drive selections by listening to single mouse clicks. The problem with this solution is that it wouldn’t capture keyboard-driven selections. tree.addKeyListener (new KeyAdapter () { public synchronized void keyTyped (KeyEvent e) { if (e.getKeyChar () == 'u0012') { TreePath path = tree.getSelectionPath (); if (path != null) { DefaultMutableTreeNode node; node = (DefaultMutableTreeNode) path.getLastPathComponent (); if (node.isRoot ()) { loadAllThreads (); } else if (((DefaultMutableTreeNode) node.getParent ()).isRoot ()) { loadThreadArticles (node); tree.setSelectionPath (path); tree.expandPath (path); } } } } }); The only problem with our interface so far is that we still rely on double-clicks to refresh threads, so it’s impossible to use the interface with a keyboard exclusively.To solve this problem, we add the above key listener to the tree. The listener listens to the tree for Ctrl-R keypresses. If a user presses Ctrl-R when a root is selected, the server is queried for a refresh. synchronized void loadAllThreads () { Hashtable threads = com.loadAllThreads (); DefaultMutableTreeNode root = new DefaultMutableTreeNode ("ForumServer"); treeModel = new DefaultTreeModel (root); Enumeration threadNames = threads.keys (); int i = 0; while (threadNames.hasMoreElements ()) { String name = (String) threadNames.nextElement (); treeModel.insertNodeInto (new DefaultMutableTreeNode (name), root, i); i++; } tree.setModel (treeModel); } Now we’re into the code that does the work of loading data from the server into the tree. The first method to look at is loadAllThreads(), which hits the server to get its thread listing.The listing is transferred into the tree’s data model, which contains the data the JTree displays. Each time the threads are loaded from the server, a new data model instance is created and populated with the threads retrieved from the server.The method first makes a call to the communications layer, which actually implements the networking that returns the listing from the server. The keys from the Hashtable this call returns are then inserted into the tree under the root node. If the call to the server fails or if there are no threads on the server, nothing is inserted into the data model. synchronized void loadThreadArticles (DefaultMutableTreeNode thread) { thread.removeAllChildren (); int childCount = treeModel.getChildCount (thread); for (int i = 0; i < childCount; i++) { DefaultMutableTreeNode child = (DefaultMutableTreeNode) treeModel.getChild (thread, i); treeModel.removeNodeFromParent (child); } String t = (String) thread.getUserObject (); Vector arts = com.loadThreadArticles (t); Enumeration articles = arts.elements (); int i = 0; while (articles.hasMoreElements ()) { ForumArticle art = new ForumArticle ((String) articles.nextElement ()); treeModel.insertNodeInto (new DefaultMutableTreeNode (art), thread, i); i ++; } treeModel.reload (); } The loadThreadArticles(...) method accepts a thread node as an argument and queries the server to get its articles. First, though, the old article children must be removed, which is accomplished in the first part of the method.Each article text that comes over the wire is encapsulated in its own instance of ForumArticle, which itself is contained in a new DefaultMutableTreeNode as the “user object” of that node.The reload() call tells the model that something has changed and that it must fire change events to its listeners, in this case the JTree. synchronized void postArticle (String artText) { DefaultMutableTreeNode thread; TreePath path = tree.getSelectionPath (); if (path != null && !artText.equals ("")) { String id = idField.getText (); if (id.equals ("")) { display.setSelectedComponent (idTab); } else if (path.getPathCount () > 1 ) { thread = (DefaultMutableTreeNode) path.getPathComponent (1); com.postArticle (id + ForumArticle.DELIMITER + artText, thread.toString ()); loadThreadArticles (thread); tree.expandPath (path.getParentPath ()); } } } The postArticle method first determines which thread is the target of the post. Then, it looks to see if the id field contains the user’s name and confirms that there is indeed something to post. Finally, the method calls the communications object with the article text and the thread name as arguments, reloads the thread, and expands the display to the selected thread. void quoteReply () { StringBuffer reply = new StringBuffer (); String art = readArea.getText (); int idx = 0, nIdx; while ((nIdx = art.indexOf ("n", idx)) >= 0) { reply.append (QUOTE_CHARS).append (art.substring (idx, nIdx + 1)); idx = nIdx + 1; } if (idx < art.length ()) reply.append (QUOTE_CHARS).append (art.substring (idx)); reply.append ("nn"); replyArea.setText (reply.toString ()); } This method simply copies the contents of the readArea into the replyArea and prepends quote characters to each line. JButton send = new JButton ("Send Reply"); send.addActionListener (new ActionListener () { public void actionPerformed (ActionEvent e) { postArticle (replyArea.getText ()); } }); display.addChangeListener (new ChangeListener () { public void stateChanged (ChangeEvent e) { Component selTab = display.getSelectedComponent (); if (selTab == replyTab) { quoteReply (); } } }); These two listener additions are part of the layOutDisplay() method. The first is a run-of-the-mill action listener added to the Send Reply button that appears at the bottom of the Reply tab area.The second is a bit more interesting; it’s a change listener attached to the tabbed pane to detect when the Reply tab has been selected by the user. When this occurs, the reply text area is set up with quoted text by the quoteReply() method. JSplitPane split = new JSplitPane ( JSplitPane.HORIZONTAL_SPLIT, treeScroller, display); Our final modification adds the JSplitPane to the content pane. We construct the split pane using the scroll pane that contains the tree and the display tabbed pane as arguments.The ForumArticle classThe ForumArticle class is a simple encapsulation of a Forum article. It is intended for use with the 1.0 Forum Networking API and the DefaultMutableTreeNode.import java.util.*; public class ForumArticle { static final String DELIMITER = "u0000"; String id; String text; public ForumArticle (String s) { StringTokenizer strtok = new StringTokenizer (s, DELIMITER); id = strtok.nextToken (); if (strtok.hasMoreTokens ()) text = strtok.nextToken (); else { text = id; id = "<unsigned message>"; } } public String toString () { return id; } public String getText () { return text; } } The ForumArticle takes in its constructor a string that is retrieved from the server. This string normally consists of an id substring, a delimiter, and a message-body substring. Unsigned articles lack the id and delimiter, but we handle this case in the constructor by signing any unsigned articles as “unsigned message.”The toString() method is for use with the DefaultMutableTreeNode, which displays the return value of this call in the tree view.DeploymentDeployment of the completed Swing Forum is the same as in the last installment, with the addition of setting up the server. Refer to the last installment for details on deploying the client.The deployment procedure consists of only two steps:Deploy the clientStart the server from its deployment directoryFor a Web deployment, deploy the server on the Web server and make sure no firewall is blocking TCP 5000 (or whichever port you specify in the forum.cfg file).If you have insurmountable firewall issues, you will have to use a nonsockets server and matching client networking layer. The servlet-based networking solution we developed in previous columns would be a good start.ConclusionThe fully functional Swing Forum was pretty simple to produce based on the original code. All that was required was networking, which we reused, and some event handlers to enable reading and posting. The majority of our effort focused on the JTree component, which allows the user to drive the interface. Reuse is good.You can use the code developed in this article as a model for your own Swing-based GUIs. It will be especially useful for interfaces that use a JTree. JavaSoftware DevelopmentTechnology Industry