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.
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
<b>javax.swing.JApplet</b>
<b>javax.swing.JWindow</b>
java.awt.Dialog
<b>javax.swing.JDialog</b>
java.awt.Frame
<b>javax.swing.JFrame</b>
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.
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 + "rn");
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:
- Create a new
ConsoleInterface
(theJFrame
) - Initialize
ConsoleInterface
via a method used for that purpose - 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.
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.
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;
}
}
Implementing these interfaces is a breeze compared to the connection class’s thread management. Listing 2 demonstrates how to write an exec bean that executes a program on its local host machine and creates a new message containing the program’s standard output. You could extend this bean to execute actual operating system commands by having it write the incoming message text to a batch file or shell script and then execute the script.
Implement the message generation
JMS messages come in five standard flavors: BytesMessage
, MapMessage
, ObjectMessage
, StreamMessage
, and TextMessage
. You can pick the message type best suited to your data transfer or create your own custom message type. MapMessages
contain a set of key-value pairs like a HashMap
. The names of the other types should be self-explanatory.
Choosing message types for particular components can have long-lasting consequences, so a careful design is essential. The real test of your infrastructure design, however, is not so much message type selection as queue configuration. You need to make a comprehensive plan for organizing your application communications into queues and topics. This may seem onerous at first, but it is actually convenient compared to, say, working in CORBA, where much of this logic is embedded in code beyond the reach of easy configuration.
Listing 2 illustrates the basic procedure for both receiving and sending point-to-point messages. In a message-driven bean, receiving the message is trivial because it comes in as a parameter to onMessage()
. Sending a message requires more setup. Use JNDI (Java Naming and Directory Interface) to latch on to the starting points (the connection factory and the queues). This is the most likely point of failure in case of a misconfiguration, so error trapping is necessary. Once you do that, use QueueSession
to create the message and QueueSender
to transmit it.
Integrate your own console
If you prefer the basic console, there are two approaches to integrating it with your own application, depending on whether or not the console needs to be independent of the application. If the console is merely a frontend to the application and can run as part of the same program in its VM, then you don’t need the ConsoleConnection
class. Change the ApplicationController
‘s sCommand()
method so that it implements all the commands that your application supports and move the MessageEvent
class to the ApplicationController
, which can create output messages.
If the console should run remotely from the application, then use the basic console as is. In this case, you need to add the CommandServer
and CommandListener
classes to your server-side application component packages.
If you are already using JMS or want to adopt a messaging platform for your console communications, then you should upgrade to the extended JMS console. A JMS-based console offers the most reliable and flexible way to communicate with your servers and is especially appropriate if you operate in a highly distributed enterprise environment. By combining JMS with Swing you have a strong foundation for rapid application development and unparalleled control over your J2EE server-side systems.