Implement a J2EE-aware application console in Swing

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

1 2 3 Page 2
Page 2 of 3

Thankfully, there is a much easier way. Listing 1 shows how to use getResourceAsStream() to load the icon from wherever the JFrame class is located. This approach works equally well whether your classes are in a jar file or sitting loose in a directory.

Set up the components

To set up components in the JFrame, first get a reference to its content pane. Your root component's content pane is the underlying foundation to which everything else will be added. In Listing 1, I first configure the components to add the console panel and the text area, and to add them only after configuration is complete. This is a good practice. I add the panel to the content pane, the text area to the scroll pane, and the scroll pane to the panel. Using a JPanel as the container for the text area's scroll pane rather than adding the scroll pane directly to the content pane makes controlling the scroll pane's margin easier. It also offers greater flexibility if I later change the interface, because the JPanel can group multiple controls that go together. For example, later in the article we will seamlessly add tabs to our interface just by adding the JPanel to a JTabbedPane.

In this project's code, most objects are created as null or using empty constructors and then configured later. For example, we create the JScrollPane using the no-parameter constructor and set its interior object later using the setViewportView() method. This differs from typical tutorial-style examples, which usually use a constructor with a component parameter, thus setting up the JScrollPane and its view in one statement.

There are a couple of reasons why I prefer the empty constructor (or initializing to null). One is to organize code so that initialization operations on a given object exist in one place. Another is to keep as much functionality as possible within error-trapped regions rather than in untrapped class initializers. When writing commercial-quality code, you should follow this guideline as a best practice: keep object configuration and manipulation out of untrapped regions, such as class initializers.

The next step is to set up listeners for the interface. Listeners are classes that respond to events, such as the user entering text or clicking the mouse. Because they are typically small and used only in one place, listeners work well as anonymous classes.

In our case, we need two listeners: one to react when the user clicks the JFrame's close control, and the other to respond to user input. Because our console has only one window, when the user closes it we call a method in the application control class to terminate the application. We don't want to put the System.exit() call right in the listener itself, because there might be some cleanup that only the main class can do. As a general principle in writing MVC applications, methods in the interface classes should only operate on their own components and should delegate all other tasks to the main control class.

Resizing, focus, and visibility

The ConsoleInterface's vResize() method in Listing 1 shows one way to size the JFrame to fill the screen. It uses the platform toolkit (accessible via java.awt.Toolkit) to determine the screen size and resize the JFrame to those dimensions. The toolkit has a number of useful methods for getting information about the host system. You may want to experiment with its capabilities.

Another ancillary but important interface activity is setting the focus. A console has such a simple set of components that managing the focus won't come into play. In a multifaceted application, focus can be a much larger problem. Here, we only need the command area to call requestFocus(). Note that this will only work after the component becomes visible.

The ApplicationController manages visibility. This is primarily due to the threading issues described earlier; however, even if the interface component could make itself visible without causing problems for the main thread, it would still be better design to have the controller manage visibility. Imagine that, instead of having one top-level frame, the application had five or six. Would you want each frame spontaneously deciding when to make itself visible? Probably not. In an MVC design, you should centralize the application's top-level behavior in the controller.

Extend the interface to include tabs

The basic console constitutes a fully functional command interface, but eventually we may want to extend it to include more customized displays. The most natural way to do this is to incorporate tabbed panes. The user can then flip to the command tab to use the console or to other tabs with implementation-specific graphics or controls. In the source code, there is a second package called extendedconsole that implements tabbed panes and other advanced features.

Because the basic console's command components rest on a JPanel, you can easily convert the console to use tabs by adding the JPanel to the JTabbedPane, which is then added to the main content pane:

private final JTabbedPane jtpMain = new JTabbedPane();
content.add(jtpMain);
jtpMain.addTab("Console", panelConsole);
jtpMain.addTab("Graphics", null);

In our example project, the second tab is left empty; in real-world use, it would typically have graphics, custom controls, or command windows for additional connections.

Make the console browser-ready

Due to the slow advance of browser technology, viewing Swing applets is still problematic (although the situation is improving). As of this writing, there are three major Swing-ready browser environments: Netscape Navigator 6, Microsoft Internet Explorer 5 on a Mac with MRJ, and Opera 5. Other browsers require the Java plug-in to run Swing applets. In my testing, I used Opera.

To make the console applet-ready, have ApplicationController extend JApplet, make ApplicationController's constructor public, and then override JApplet's start() method like this:

  public void start() { vStartConsole(); }

That's all there is to it. The console still behaves normally as an application because, when started as an application, the main() method is used instead of the start() method. In the HTML page that shows the applet, you need a tag that loads the jar file. One possible tag form is:

      <applet 
            archive  = extendedconsole.jar
            code     = extendedconsole.ConsoleController 
            codetype = "application/java">

Your Web server must have this jar file in its document root or you must specify its path in the archive tag. Note that the window closer won't work in an applet context because the container (i.e., the browser) controls the applet virtual machine's start and shutdown.

If you develop applets, you will find it extremely useful to have an IDE, like JBuilder, that can load applets via an HTML page into the debug environment.

Introducing JMS

JMS is the Java 2 Platform, Enterprise Edition (J2EE) API for asynchronous, message-based communications. To use it, you will need a message broker such as iPlanet's Java Message Queue; I used this broker's 2.0 version for this article. Different JMS implementations include a wide variety of features but the example code should work with any platform.

A primary advantage of JMS is loosely coupled reliability. This makes JMS well suited for informational systems like an enterprise console. Machines and networks can go up and down, but the consoles keep ticking. The ease of broadcasting informational channels is another key reason for basing your console communications on JMS. Finally, J2EE-compliant application components implement a JMS interface automatically. This makes integration with a JMS-enabled console especially easy, because your 2.0 EJBs are ready to send and receive console command messages from the get-go.

The basic strategy for using JMS in a command and query model is twofold: use point-to-point messaging for command and response between a console and a component, and use publish/subscribe messaging when a component needs to broadcast an information channel to an indefinite number of console listeners. The overall architecture of the JMS approach is shown in Figure 3.

Figure 3. The architecture of a JMS-based console system

From the diagram, you can see that the message queue is the heart of the architecture. It allows developers or system operators/admins to communicate with the application objects in a loosely coupled, yet reliable, way. Objects can also broadcast a steady stream of information to consoles that subscribe to their topics.

Reconstruct the connection class for JMS

In the extended console example, the connection class, which manages the console's I/O (input/output), is completely reconstructed for JMS. The original connection class is a typical socket-oriented communication class that extends Thread. After being reworked for JMS, it has the same skeleton: it extends Thread and has the same API and logic, but the socket-related calls have been replaced with messaging operations.

Because messaging is more straightforward than socket I/O, the code is simpler. In particular, a lot of the exception handling necessary for sockets has been eliminated. You can compare ConsoleConnection.java in the basicconsole package to the same class in the extendedconsole package to see how they differ. For a standalone application like the console, setting up a thread to block as it waits for new messages is a parallel approach to socket communications. For the server-side components, however, the best approach is to implement a message-driven bean.

Create your message-driven beans

In the EJB 2.0 specification, Sun introduced message-driven beans. Message-driven beans receive messages from their container. This makes responding to messages easier and eliminates the need for the ConsoleConnection class framework described above. To turn a component into a message-driven bean, implement two interfaces, MessageDrivenBean and MessageListener, as shown in Listing 2:

Listing 2. ExecBean.java

/* The exec bean receives a text message that it treats as 
 * the path of a program or script to be executed on the 
 * local machine. It creates and sends a new message 
 * containing the program's output.
 */
import javax.ejb.MessageDrivenBean;
import javax.ejb.MessageDrivenContext;
import javax.naming.*;
import javax.jms.*;
import java.io.*;
public class ExecBean implements MessageDrivenBean,
                                 MessageListener     {
  private MessageDrivenContext mContext;
  private javax.naming.Context contextJNDI;
  private String                  sQueueName = null;
  private final String  QCF = "QueueConnectionFactory";
  private QueueConnectionFactory  queueConnFactory = null;
  private QueueConnection         queueConnection = null;
  private QueueSession            queueSession = null;
  private Queue                   queue = null;
  private QueueSender             queueSender = null;
  public ExecBean() {}
  public void ejbRemove() {}
  public void setMessageDrivenContext(
     MessageDrivenContext context) {
    mContext = context;
    try { // this setup is only needed if sending messages
      StringBuffer sbError = new StringBuffer(250);
      Object oEntity = jndiLookup(QCF, sbError);
      if (oEntity == null) {
        System.err.println("JNDI lookup failed: "+ sbError);
        return;
      }
      queueConnFactory = (QueueConnectionFactory)oEntity;
      queueConnection =
         queueConnFactory.createQueueConnection();
      queueSession =
         queueConnection.createQueueSession(false,
                Session.AUTO_ACKNOWLEDGE);
      Object oQueue = jndiLookup(sQueueName, sbError);
      if (oQueue == null) {
        System.err.println("JNDI lookup failed: "+ sbError);
        return;
      } else {
        queue = (javax.jms.Queue)oQueue;
      }
      queueSender = queueSession.createSender(queue);
    } catch(Exception ex) {}
    if (queueSender == null)
       System.err.println("Unable to send messages.");
  }
  public void onMessage(Message theIncomingMessage) {
    try {
      if (theIncomingMessage instanceof TextMessage) {
        TextMessage tm = (TextMessage)theIncomingMessage;
        Process p = Runtime.getRuntime().exec(tm.getText());
        BufferedReader br = new BufferedReader(
                               new InputStreamReader(
                                  p.getInputStream()));
        StringBuffer sbResponse = new StringBuffer(250);
        String sLine;
        while ((sLine = br.readLine()) != null) {
          sbResponse.append(sLine);
        }
        if (queueSender == null) { // can't send message
          System.out.println(sbResponse.toString());
        } else {
          tm = queueSession.createTextMessage();
          tm.setText(sbResponse.toString());
          queueSender.send(tm);
        }
      }
    } catch(Exception ex) {}
  }
  // The output queue must be specified before outbound
  // messages can be sent.
  public void setQueueName(String sNewQueueName) {
    sQueueName = sNewQueueName;
  }
  private Object jndiLookup(String sName,
    StringBuffer sbError) {
    Object oEntity = null;
    if (contextJNDI == null) {
      try {
        contextJNDI = new InitialContext();
      } catch (Exception ex) {
        sbError.append("Failed to create JNDI context: " +
           ex.toString());
        return null;
      }
    }
    try {
      oEntity = contextJNDI.lookup("cn=" + sName);
    } catch (Exception ex) {
      sbError.append("JNDI lookup failed for:" + sName +
         ": " + ex);
      return null;
    }
    return oEntity;
  }
}
1 2 3 Page 2
Page 2 of 3