Implement a J2EE-aware application console in Swing

Use JMS to query and control your enterprise application from a Swing console

An essential part of complex enterprise applications is a console. Consoles are the simplest but most flexible user interface. They provide a window that allows the developer or system operator to type a command and receive a text response. Operating systems invariably offer a system console; even the Mac has one, called the Macintosh Programmer's Workshop (MPW).

In an enterprise or application service provider (ASP) environment, the console provides a window into a system's operation and allows operators to configure and control the system in real time. The console can also display unprompted status information and announcements.

In this article, I'll show you how to construct a generic console from Swing components that uses the Java Messaging Service (JMS) to interact with one or more application subsystems. JMS provides a standard solution to the problem of communication between the backend system's command servers and their clients.

Figure 1 shows the console client's on-screen appearance. The user types commands in the interface's lower box (JTextField) and receives an answer in the text area above it. The dot before the command indicates that the command goes to the console itself rather than to the connected host or queue. When text fills the text area, scroll bars automatically appear.

Figure 1. The console in action with the default Metal look and feel

Rather than create a distributed communication system in one step, we will build up to the finished product in stages. First, we will construct a basic console that operates synchronously using a socket. This introduces the Swing fundamentals and demonstrates how to build a functional console without the extra JMS complexities. Then we will extend our basic console with useful features, such as tabbed panes. Finally, we will add JMS for industrial-strength communications.

Before getting started

The biggest hurdle to starting with Swing is understanding its underlying object model. Your application or applet must sit above a complex web of classes with subtle and hidden interactions. Later in this article, I will cover some other important topics, such as event threads and peers. To get started, you just need to know the component model. Mastering this foundation takes time, but the inheritance outline below may give you a leg up on the learning curve:

Swing's inheritance model

java.awt.Component (abstract)
      java.awt.Container
            javax.swing.JComponent (abstract)
                  javax.swing.JRootPane
                  ...other concrete components (JPanel, JTree etc.)
            java.awt.Window
                  java.awt.Panel
                        java.applet.Applet
                              javax.swing.JApplet
                  javax.swing.JWindow
                  java.awt.Dialog
                        javax.swing.JDialog
                  java.awt.Frame
                        javax.swing.JFrame

The top-level Swing components (JApplet, JWindow, JDialog, and JFrame) are in bold above. Each of these containers has only one component, JRootPane. The diagram below illustrates the JRootPane -- the key to Swing.

Figure 2. The organization of Swing's JRootPane

The root pane has a glass pane on top and a layered pane underneath. The layered pane is composed of a MenuBar and a ContentPane. You add subcomponents to your outer container via the content pane. When you use a component's getContentPane() method, you are actually getting its root pane's content pane.

Because the GlassPane component is always painted last, it appears on top of the ContentPane and MenuBar. The GlassPane lets you shield the underlying layered pane from mouse clicks. You can also use it for graphical overlays. For example, you can move special cursors or animation sprites (images moved on a stationary background) around the glass pane without disturbing the main content.

The console's design is Model-View-Controller

When writing applications with a visual interface, such as a console, you should structure them with the Model-View-Controller (MVC) design in mind (see Design Patterns for a discussion of this architecture). Our example console has three core classes: ApplicationController (the controller), ConsoleInterface (the view), and ConsoleConnection (the model). Strict adherence to the design pattern is not necessary, but you should segregate the application into logical parts that you can write and maintain separately from one another. Many textbook examples of similar applications make the mistake of stuffing everything into one class that implements Runnable. They do this to avoid the tricky problem of passing information between threads, but in the process they teach the wrong approach to Swing. I will show how you can solve this problem and preserve a good design as we build the example console.

The command servers -- the server-side components that listen and respond to console connections -- are an important part of the overall design. CommandServer.java, an example class found in this article's source code, implements a typical command server. The command server listens on a server socket for connections and then acts as an intermediary between the consoles and the server-side components. When using a synchronous server in this way, the command server must act like a messaging router in case the consoles want to access multiple information sources.

Set up the top-level component

Out of the four possible top-level components -- JApplet, JWindow, JDialog, and JFrame -- the JFrame is the best and most full-featured component for creating typical application windows. JWindow has no title bar so it is most commonly used for splash screens, progress bars, and other plain boxes. To create the top-level element, therefore, we first make our interface class, ConsoleInterface, extend JFrame. Listing 1 shows this class in its entirety:

Listing 1. ConsoleInterface.java

/* The interface class plays the role of the view in the
 *  basic console's Model-View-Controller architecture.
 *  The processEvent() method overrides processEvent in the
 *  JFrame's parent container to provide a way for input to 
 *  reach the interface.
 */
package basicconsole;
import java.io.InputStream;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
public class ConsoleInterface extends JFrame {
  private final static int iDEFAULT_FontSize = 12;
  ConsoleInterface() {}
  private final JPanel panelConsole = new JPanel();
  private final JTextArea jtaDisplay = new JTextArea();
  private final JTextField jtfCommand = new JTextField(32);
  private final JScrollPane jspDisplay = new JScrollPane();
  boolean zInitialize(String sTitle, StringBuffer sbError){
   try{
    // create icon
    try {
      byte[] abIcon;
       InputStream inputstreamIcon =
          this.getClass().getResourceAsStream("icon.gif");
       int iIconSize = inputstreamIcon.available();
       abIcon = new byte[iIconSize];
       inputstreamIcon.read(abIcon);
       this.setIconImage(new ImageIcon(abIcon).getImage());
    } catch(Exception ex) {
        // the default icon will be used
    }
    this.setTitle(sTitle);
    // set up content pane
    Container content = this.getContentPane();
    content.setLayout(new BorderLayout());
    panelConsole.setLayout(new BorderLayout());
    content.add(panelConsole);
    // set up display area
    jtaDisplay.setEditable(false);
    jtaDisplay.setLineWrap(true);
    jtaDisplay.setMargin(new Insets(5, 5, 5, 5));
    jtaDisplay.setFont(
         new Font("Monospaced", Font.PLAIN, iDEFAULT_FontSize));
    jspDisplay.setViewportView(jtaDisplay);
    panelConsole.add(jspDisplay, BorderLayout.CENTER);
    panelConsole.add(jtfCommand, BorderLayout.SOUTH);
    // listener: window closer
    this.addWindowListener(
       new WindowAdapter(){
          public void windowClosing(WindowEvent e){
             ApplicationController.getInstance().vExit();}
    });
    // listener: command box
    jtfCommand.addActionListener(
       new ActionListener(){
          public void actionPerformed(ActionEvent e){
             String sCommandAnswer =
             ApplicationController.getInstance().sCommand(
             jtfCommand.getText());
           vDisplayAppendLine(sCommandAnswer);
           jtfCommand.setText("");
           }
         });
    this.vResize();
    return true;
    } catch (Exception ex){
      sbError.append(
        "Error initializing control panel: " + ex);
        return false;
    }
  }
  void vResize(){
    Dimension dimScreenSize =
       Toolkit.getDefaultToolkit().getScreenSize();
    this.setSize(dimScreenSize);
  }
  void vDisplayAppendLine(String sTextToAppend){
    jtaDisplay.append(sTextToAppend + "\r\n");
    try { // scroll to end of display
      jtaDisplay.scrollRectToVisible(
         new Rectangle(0,
            jtaDisplay.getLineEndOffset(
               jtaDisplay.getLineCount()), 1,1));
    } catch(Exception ex) {}
  }
  protected void vSetFocus() {
    jtfCommand.requestFocus();
  }
  protected void processEvent (AWTEvent e) {
    if (e instanceof MessageEvent ) {
      vDisplayAppendLine(((MessageEvent)e).getMessage());
      return;
    }
    super.processEvent(e);
  }
}

To see the source for the other two classes, ApplicationController and ConsoleConnection, you must download the source code. As soon as the main thread news the ConsoleInterface class (newing creates an instance of an object; the term derives from the Java keyword new), our interface will come into existence (but it won't appear on-screen because it is not yet visible). The strategy for bootstrapping the interface from the main thread is:

  1. Create a new ConsoleInterface (the JFrame)
  2. Initialize ConsoleInterface via a method used for that purpose
  3. Invoke ConsoleInterface.visible(true) to make it appear

The initialization method returns true or false depending on whether the interface initialization succeeds. This allows the main thread to recover if for some reason the JFrame could not initialize. The snippet below illustrates how the main thread activates the interface:

if (mInterface.zInitialize(DEFAULT_TITLE, sbError)) {
  mInterface.setVisible(true);
} else {
  javax.swing.JOptionPane.showMessageDialog(null,
     "Failed to initialize interface: " + sbError);
}

If the interface could not initialize, a dialog box appears telling the user why. JOptionPane's ability to create such message dialogs is one of Swing's most convenient features. Lastly, the main thread makes the interface appear by setting its Visible property.

By saying "lastly," I really mean lastly. It is important that the main thread do nothing further with the JFrame, because as soon as the interface becomes visible, its peer is created and the JFrame and its subcomponents become candidates for paint events. The peer is the native platform object that the Swing component generates. For example, on a Windows machine, a JFrame's peer is the window memory structure that actually displays on the screen. Since access to the peer is not thread safe and paint events occur on the event-dispatching thread, a conflict could occur if you try to access the JFrame on the main thread. This mistake is the most common cause of instability in Swing applications.

The state of becoming paint-ready is called realization. Because the main thread cannot safely access the interface after it is realized, the interface's initialization method should not call any method that can cause realization, such as setVisible(), show(), or pack(). As long as the application controller has a monopoly on interface realization, it can maintain thread safety by making sure that it always causes realization last when creating any top-level component. Once a component is realized, all subsequent activity on that component must take place on the event-dispatching thread.

To put something on the event-dispatching thread, you use the SwingUtilities.invokeLater() method. For example, we should set the interface's initial focus, but can do so only after the JFrame becomes visible. For this action to occur on the event-dispatching thread, we create a small anonymous class (see the "All About Anonymous Classes" sidebar) that implements Runnable, and then we ask the event-dispatching thread to run it like this:

SwingUtilities.invokeLater(
  new Runnable() {
    public void run() {
      mInterface.vSetFocus();}});

The portion of this statement beginning with new Runnable() defines an anonymous class, which initializes the focus when it runs. By passing this anonymous class to invokeLater(), the main thread asks that the class be run later on the event-dispatching thread.

Now that we understand how to create the interface, we can tackle the job of adding content to it.

Add an icon

You need an icon for your application to give it personality and easier user recognition. Too many book examples and even commercial applications have the default coffee cup. The likely reason for the widespread lack of icons is that there is no easy or obvious approach to implementing them. One book, whose title I won't identify here for the sake of its author, recommends that you put the path to your icon files in a properties text file and then have your installer set each path to the correct value when the program installs.

1 2 3 Page 1
Page 1 of 3