Start saddling up for Mustang

news
Jan 9, 200636 mins

Explore some of the new features in Java SE 6

Sun will release its newest Java platform — also called Mustang — later this year. Officially known as Java Platform, Standard Edition 6 (Java SE 6 — read “Building and Strengthening the Java Brand” for the reason behind the name change), Mustang focuses on several major themes, such as compatibility and stability. For a complete list of themes, see Java Specification Request 270.

New features expected to be part of Mustang include (among others):

  • An API for accessing the compiler
  • Console input/output (I/O)
  • An API for working with splash screens
  • Numerous Java 2D performance improvements
  • XML Digital Signatures API
  • An API for interacting with the system tray
  • Partition-space methods in the java.io.File class
  • Java Database Connectivity (JDBC) 4.0
  • Common annotations
  • Scripting API
  • A streaming API for XML
  • The ability to sort and filter the contents of javax.swing.JTables
  • Programmatic access to network parameters (such as the broadcast address and the network mark)
  • The ability to conveniently print the contents of javax.swing.text.JTextComponents

Mustang has far too many new features to explore in one article, so, instead, I focus on a small subset of features. Specifically, I discuss console I/O, partition-space methods, and the APIs for working with splash screens and for interacting with the system tray. I also illustrate the new JDBC 4.0 automatic driver-loading feature.

Note: I originally created and tested this article’s code using Java SE 6 version 1.6.0-rc (build 62). The underlying platform was Microsoft Windows ME. I subsequently updated this article to reflect version 1.6.0 (build 105), the December 11, 2006 release version of Java SE 6.

Console I/O

In May 1997, Sun received a request for enhancement (RFE) to improve console I/O. Specifically, the requester asked for a way to prompt users for a pass-phrase and to let users type that pass-phrase (of arbitrary length) without the characters being echoed to the console. The requester pointed out that the Abstract Window Toolkit’s (AWT) setEchoChar() method is not usable because it relies on a GUI’s availability, and many server-based operating systems don’t use GUIs.

In late 2005, Sun responded to RFE #4050435 by adding the java.io.Console class to Mustang (build 57). That class provides methods for accessing the character-based console device associated with the current virtual machine. But before you can call those methods, you must obtain a Console object by invoking System‘s public static Console console() method. That method returns either a Console object for interacting with the console device or null if the console device does not exist—the console device does not exist if you redirect standard input or standard output (or both), for example. After invoking System.console() to return a Console object, the following code fragment determines whether the console device exists by testing the returned Console reference for null:

Console console = System.console ();
if (console == null)
{
    System.err.println ("Console not available");
    return;
}

Assuming that the console device is available, you can read passwords and lines of characters from the console input stream, and write characters to the console output stream. To read a password (without echoing password characters to the console output stream), you must invoke either one of Console‘s two readPassword() methods. These methods do not include any line-termination characters as part of the password; they will return null if the end of the console input stream is reached.

For example, you can call public char [] readPassword(String fmt, Object... args) to prompt the user to type a password, where the prompt is described by the java.util.Formatter-style format string (fmt) and its variable list of arguments (args), and then return the user’s chosen password in a character array. The code fragment below repeatedly invokes readPassword(String fmt, Object... args) to prompt the user to type a password until the user enters a password that’s at least MIN_PWD_LEN characters in length:

char [] pwd; do {

pwd = console.readPassword ("Enter password (%d characters min):", MIN_PWD_LEN); } while (pwd.length < MIN_PWD_LEN);

After the password is stored in pwd, it can be used as required. However, for security reasons, pwd should be zeroed out when the password is no longer needed.

In addition to the readPassword() methods, Console provides a pair of readLine() methods for conveniently reading an entire line of characters from the console and storing those characters (minus line-termination characters) in a String. These methods will return null if the end of the console input stream is reached.

For example, you can call public String readLine() to return a line of characters without prompting the user. This method is demonstrated by the following code fragment:

 String input = console.readLine ();
Note
The Console object is associated with a unique java.io.Reader object and a unique java.io.PrintWriter object. Invoke Console‘s public Reader reader() method to return the Reader. You can then pass the Reader to one of java.util.Scanner‘s constructors to perform sophisticated parsing of the console input stream, for example. To obtain the PrintWriter, invoke Console‘s public PrintWriter writer() method. You can then invoke a variety of useful methods for outputting different typed data to the console. For convenience, Console provides a public void flush() method that invokes PrinterWriter‘s flush() method.

There is something interesting about both readPassword() methods and both readLine() methods. When these methods encounter a problem with I/O, they do not throw a java.io.IOException object (which is thrown by System.in.read(), for example). Instead, each method throws a java.io.IOError object. Because IOError subclasses Error, you do not need to catch this object, unlike IOException.

The counterpart methods for outputting characters to the console output stream are public Console format(String fmt, Object... args) and the equivalent public Console printf(String fmt, Object... args) convenience method, which internally invokes format(). The code fragment below demonstrates printf():

console.printf ("%s", input);

After outputting characters, format() — and, by extension, printf() — automatically flushes the console output stream.

Let’s put our knowledge of console I/O to some practical use—database access. To that end, I’ve created a Microsoft Access sales.mdb sales database, which is distributed with this article’s code (downloadable from Resources). This database contains a single territories table with two columns: Name, for a salesperson’s name, and Territory, the locations where the salesperson can legally sell products. I’ve populated territories with the data below, and I’ve password-protected the database — mustang is the password.

| ------------------------ |
| Name        | Territory  |
| ------------------------ |
| John Doe    | North      | 
| Jane Doe    | South      | 
| Paul Smith  | East       |
| Kathy Smith | West       |
| ------------------------ |

The sales database will be accessed from a Sales application. Prior to accessing the database, the application requests a username and password. It then uses these values in an attempt to connect, via the JDBC-ODBC bridge driver, to the sales datasource (which identifies the location of sales.mdb and which you create with the Windows Control Panel ODBC Data Sources applet). If the connection succeeds, the application outputs the values in all rows and columns of the territories table. Listing 1 presents the source code to the Sales application.

Listing 1. Sales.java

// Sales.java

import java.io.*; import java.sql.*; import java.util.*;

class Sales { public static void main (String [] args) throws ClassNotFoundException, SQLException { // Attempt to obtain a console.

Console console = System.console (); if (console == null)

{ System.err.println ("sales: unable to obtain console"); return; }

// Obtain username.

String username = console.readLine ("Enter username: ");

// Obtain password.

String password = new String (console.readPassword ("Enter password: "));

// Create a Vector datastructure for holding table records.

Vector<Object []> v = new Vector<Object []> ();

Connection con = null; Statement stmt = null; ResultSet rs;

try { // Attempt to connect to the sales datasource.

con = DriverManager.getConnection ("jdbc:odbc:sales", username, password);

// Garbage collect the password—not a good idea to keep passwords // hanging around.

password = null;

// Attempt to create a statement.

stmt = con.createStatement ();

// Establish the maximum number of rows that can be returned.

stmt.setMaxRows (10);

// Attempt to fetch all rows from the territories table.

String query = "SELECT * FROM territories"; rs = stmt.executeQuery (query);

// Get number of columns.

int nCols = rs.getMetaData ().getColumnCount ();

// Read all rows of all columns into storage.

int i = 0;

while (rs.next ()) { Object [] buffer = new Object [nCols];

for (int j = 0; j < nCols; j++) buffer [j] = rs.getObject (j + 1);

// NOTE: getObject requires a 1-based column index.

v.add (buffer); }

// Extract rows from the array.

Object [] rows = v.toArray (); for (i = 0; i < rows.length; i++) { // Extract columns from the row.

Object [] cols = (Object []) rows [i];

// Print out the values from each column.

for (int j = 0; j < cols.length; j++) console.printf ("%s ", cols [j]);

console.printf ("n"); }

console.printf ("Value at Row 1, Col 0 = %sn", getValue (v, 1, 0)); } catch (SQLException e) { console.printf ("sales: %sn", e.getMessage ()); } finally { if (stmt != null) try { stmt.close (); } catch (SQLException e2) {}

if (con != null) try { con.close (); } catch (SQLException e2) {} } }

static Object getValue (Vector v, int row, int col) { // The following conversion should really not be done in this method, // because it's inefficient. Placing the conversion here is a matter of // convenience.

Object [] rows = v.toArray ();

Object [] cols = (Object []) rows [row];

return cols [col]; } }

Compile Sales.java and run the application. You will be greeted with an Enter username: prompt. Type anything you like for the username, but you must enter something. You will next be prompted to type the password. Make sure to specify mustang. Assuming a connection is successfully established, you should see the table values I presented earlier.

Note
Look closely at Listing 1. The code does not try to load the JDBC-ODBC bridge driver via Class.forName(). Because Mustang includes JDBC 4.0, whose DriverManager class provides a transparent automatic driver-loading mechanism to locate and load driver classes, you no longer need to explicitly load driver classes with Class.forName().

Listing 1 uses Console‘s printf() method to output the table contents. However, this approach has a subtle problem. If the territories table had many rows of data, not all of these rows would fit within the console window without scrolling. Normally, to capture these rows, you would redirect standard output to a file. But if you do that, System.console() returns null, and you cannot use console I/O. Hence, think carefully before using Console‘s printf() and format() methods in your own applications. Don’t use them to output large quantities of data to the screen; otherwise you will not be able to view all of that data.

Although Console addresses the original RFE, many developers take issue with this class. Their criticisms include:

  • The System.console() method should have been named System.getConsole(). However, some disagree, arguing that the “get” prefix only applies to beans and System is not a bean. Furthermore, console() is a static method, which means IDEs don’t consider this method to be a property getter.
  • The Console class should have provided the capability to read single characters without buffering. In other words, a program should not have to wait for the user to press the Enter key before reading user input. A method similar to C’s kbhit() function would help address this criticism by returning a Boolean indicating whether any keys have been pressed. If a true value returns, another method would be called to return the next keystroke waiting in the BIOS keystroke buffer.
  • Console should have provided the capability to enable/disable the echoing of characters to the console. A pair of methods similar to C’s getch() (get character without echoing that character to the console and block only if no characters are waiting in the BIOS keystroke buffer) and getche() (get character and echo that character to the console and block only if no characters are waiting in the BIOS keystroke buffer) functions would address this criticism.
  • Console should have provided a screen-clearing capability and other features found in the Unix-based curses library.
  • Console should have provided a detach() method to detach an application from the console and send it into the background.

Partition-space methods

One month after receiving RFE #4050435 to improve console I/O, Sun received another RFE for providing a means to obtain free disk space. Sun responded to RFE #4057701 by adding three partition-space methods to the File class:

Note
Many modern operating systems allow a restriction on the maximum amount of disk space to which a user is entitled. This maximum is the user’s quota, and the region of disk space set aside for that user is the user’s partition. In this context, getTotalSpace() returns the quota for the user associated with the calling thread. If the user does not have a quota (there is only one user), this method returns the total number of bytes on the disk. Also, getUsableSpace() returns the number of bytes available before the quota is reached to the user associated with the calling thread. If the user does not have a quota, this method returns the total number of free bytes on a disk. Finally, based on the GetDiskFreeSpaceEx() documentation, getFreeSpace() returns the number of free bytes on the disk—regardless of a user’s quota.
  • public long getFreeSpace(): Returns the number of unallocated bytes in the partition named by this abstract pathname. This value is zero if the abstract pathname does not name a partition.
  • public long getTotalSpace(): Returns the size of the partition named by this abstract pathname. This value is zero if the abstract pathname does not name a partition.
  • public long getUsableSpace(): Returns the number of bytes available to this virtual machine on the partition named by this abstract pathname. This value is zero if the abstract pathname does not name a partition.

For the Windows version of Mustang, these methods are implemented in terms of Microsoft’s GetDiskFreeSpaceEx function. If you were to redefine the methods based on that function’s documentation:

  • getFreeSpace() would return the total number of free bytes on the current root’s disk or zero if the root refers to a CD-ROM drive. Nonzero would return if the disc was an unwritten CD in a CD-RW drive.
  • getTotalSpace() would return a disk’s total number of bytes available to the user associated with the calling thread. If per-user quotas are used, this value may be less than the total number of bytes on the disk.
  • getUsableSpace() would return a disk’s total number of free bytes available to the user associated with the calling thread. If per-user quotas are used, this value may be less than the total number of free bytes on a disk. Zero returns if the current root refers to a CD-ROM drive, and nonzero returns if the disc was an unwritten CD in a CD-RW drive.

Listing 2 shows the source code to a SpaceChecker application, which presents the free space for each root in the filesystem, and the usable and total space allotted to the current user for each root in the filesystem.

Listing 2. SpaceChecker.java

// SpaceChecker.java

import java.io.File;

public class SpaceChecker { public static void main (String [] args) { File [] roots = File.listRoots ();

for (int i = 0; i < roots.length; i++) { System.out.println (roots [i]); System.out.println ("Free space = " + roots [i].getFreeSpace ()); System.out.println ("Usable space = " + roots [i].getUsableSpace ()); System.out.println ("Total space = " + roots [i].getTotalSpace ()); System.out.println (); } } }

When I run this application on my Windows ME platform, I observe the following space information:

A:Free space = 0 Usable space = 0 Total space = 0

C:Free space = 27913584640 Usable space = 27913584640 Total space = 40965373952

M:Free space = 0 Usable space = 0 Total space = 0

N:Free space = 0 Usable space = 0 Total space = 0

The free and usable space values for the C: drive are the same because quotas do not apply to the Windows ME operating system.

The splash-screen API

Splash screens are an important part of modern GUI-based applications. While occupying a user’s attention during lengthy application startup (and possibly notifying the user of copyright and other important details), a splash screen also assures the user that the application is starting. This is especially important in a Java context, because it can take some time for the JVM to load and start running. Figure 1 illustrates an example splash screen.

Use a splash screen to display copyright and other important information
Figure 1. Use a splash screen to display copyright and other important information

Mustang’s implementation of a splash screen is an undecorated window capable of displaying a GIF (including an animated GIF), PNG, or JPEG image. The java application launcher creates the splash-screen window and displays a specified image in that window in response to either a command line option or a JAR manifest entry:

  • The -splash command line option creates the splash-screen window and displays a specified image. For example, java -splash:mylogo.gif MyApp creates the splash-screen window and displays the image identified by mylogo.gif in that window (see Figure 1), then loads and starts the JVM, and finally causes the JVM to load MyApp.class and begin execution in that class’s public static void main(String [] args) method.
  • Because you will most likely package a significant application into a JAR file, Mustang provides a SplashScreen-Image manifest entry for referencing a splash screen’s image from the JAR file’s manifest. For example, SplashScreen-Image: mylogo.gif identifies mylogo.gif as the image to be displayed in the splash-screen window.

    Assuming that the following information has been placed into a manifest file:

    Manifest-Version: 1.0
    Main-Class: MyApp
    SplashScreen-Image:mylogo.gif
    

    And that this manifest file, mylogo.gif, and all relevant class files have been packaged into myApp.jar, java -jar myApp.jar creates the splash-screen window that displays mylogo.gif before loading and starting the JVM, and running the application.

What would happen if you invoked java -splash:yourlogo.gif -jar myApp.jar? In this situation, yourlogo.gif would appear in the splash-screen window. The -splash command-line option takes precedence over the SplashScreen-Image manifest setting.

Suppose you want to customize the splash screen by introducing a visual effect on top of that image. For example, you might want to implement a dynamic progress bar over the splash-screen image to inform the user of the time remaining on a lengthy application-specific initialization task (check out the SplashTest application in “New Splash-Screen Functionality in Mustang” for a dynamic progress bar demonstration). Mustang’s java.awt.SplashScreen class supports splash-screen customization.

You cannot instantiate objects from the SplashScreen class because a SplashScreen object makes no sense without the splash-screen window, and the splash-screen window does not exist if you have not specified either the -splash command line option or the SplashScreen-Image manifest entry. But if you specify either the option or the entry, and the splash-screen window is created, Mustang creates a SplashScreen object as part of its startup activities. Call SplashScreen‘s public static SplashScreen getSplashScreen() method to return a reference to this object. Keep in mind that null returns if there is no splash-screen window. After invoking SplashScreen.getSplashScreen() to return the SplashScreen object, the following code fragment first determines if the splash-screen window exists by testing the returned SplashScreen reference for null:

SplashScreen ss = SplashScreen.getSplashScreen ();
if (ss != null)
{
    // Customize the splash screen.
}

Assuming the splash-screen window exists, you can call the following five SplashScreen methods to obtain a graphics context for drawing in a buffer, acquiring the current splash-screen image, obtaining the image’s size, replacing this image with another image, and updating the splash-screen window with the buffer’s contents:

  • public Graphics createGraphics(): Creates a graphics context for the splash-screen overlay image, which lets you draw over the splash screen. You do not draw on the main image; you draw on the image that is displayed over the main image using alpha blending. Because the drawing on the overlay image does not necessarily update the splash-screen window’s contents, you should call update() on the SplashScreen when you want to immediately update the splash screen. This method throws an IllegalStateException if called after the splash-screen window has closed.
  • public URL getImageURL(): Returns the current splash-screen image as a URL. This method throws an IllegalStateException if called after the splash-screen window has closed.
  • public Dimension getSize(): Returns the size of the splash-screen window, which is the same size as the image being displayed. The splash-screen size is automatically adjusted when the image changes. This method throws an IllegalStateException if called after the splash-screen window has closed.
  • public void setImageURL(URL imageURL): Changes the splash-screen image to the image identified by imageURL — GIF, JPEG, and PNG image formats are supported. This method returns after the image has finished loading and the window has updated. The splash-screen window is resized to the image’s size and centered on the screen. This method throws a NullPointerException if imageURL is null, throws an IOException if an error occurs while loading the image, and throws an IllegalStateException if called after the splash-screen window has closed.
  • public void update(): Updates the splash-screen window such that the overlay image’s contents are composited with the pixels displaying the splash-screen image. Source-over compositing is used so that transparent pixels in the overlay image cause the splash-screen image’s pixels to show through; opaque pixels in the overlay image hide underlying splash-screen image pixels. This method throws an IllegalStateException if createGraphics() was never called to create the overlay image or if called after the splash-screen window has closed.

Listing 3 demonstrates getSplashScreen(), createGraphics(), getSize(), and update() in a skeletal PhotoAlbum application that customizes its splash screen by drawing a red border around the splash-screen image and displaying the horizontally-centered Registering plug-ins... message.

Listing 3. PhotoAlbum.java

// PhotoAlbum.java

import java.awt.*;

public class PhotoAlbum { public static void main (String [] args) { SplashScreen ss = SplashScreen.getSplashScreen (); if (ss != null) { Graphics g = ss.createGraphics (); g.setColor (Color.red); // Color.white is the default color in my // version of Mustang.

Dimension size = ss.getSize ();

// Each border is 1% of the smaller of the image's width and height.

int borderSize; if (size.width < size.height) borderSize = (int) (size.width * 0.01); else borderSize = (int) (size.height * 0.01);

for (int i = 0; i < borderSize; i++) g.drawRect (i, i, size.width-1-i*2, size.height-1-i*2);

// Compute width and height of string in current font.

FontMetrics fm = g.getFontMetrics (); int strWidth = fm.stringWidth ("Registering plug-ins..."); int strHeight = fm.getHeight ();

// As long as string does not exceed splash-screen window limits ...

if (strWidth < size.width && 4*strHeight < size.height) { g.setColor (Color.blue); g.drawString ("Registering plug-ins...", (size.width-strWidth)/2, size.height-4*strHeight); }

// Copy overlay image to splash-screen window.

ss.update ();

try { Thread.sleep (3000); // Pause three seconds to view image. } catch (InterruptedException e) { } } } }

To demonstrate PhotoAlbum, I’ve included a palogo.jpg image with this article’s code. When you execute java -splash:palogo.jpg PhotoAlbum, you first see palogo.jpg‘s image centered on the screen. After the JVM finishes loading and starts running main(), you see the composite image (also centered) shown in Figure 2.

The logo changes its border color and notifies the user that it is registering plug-ins.
Figure 2. The logo changes its border color and notifies the user that it is registering plug-ins. Click on thumbnail to view full-sized image.

Although the splash-screen window closes automatically when the first AWT or Swing window is made visible, you might want to close the splash-screen window before that window appears or substitute your own window for the splash-screen window. The SplashScreen class provides the following three methods to help you accomplish these tasks:

  • public void close(): Hides and closes the splash-screen window, releasing any resources allocated to this window. This method throws an IllegalStateException if called after the splash-screen window has closed.
  • public Rectangle getBounds(): Returns the splash-screen window’s boundaries. These boundaries will change should you invoke setImageURL() to establish a new splash-screen image with different dimensions. This method throws an IllegalStateException if called after the splash-screen window has closed.
  • public boolean isVisible(): Returns a Boolean true value if the splash-screen window is visible; false returns after the splash-screen window closes.

If you substitute your own window, you can give that window the same origin and size as the splash-screen window by invoking getBounds(). Also, the splash-screen window closes automatically when you make your window visible.

The system tray API

The system tray is a specialized area of the desktop that displays both the current time and the icons of continually-running desktop applications, and is shared by all applications currently running on the desktop. Figure 3 presents the Windows ME system tray, which resides on the right side of the Windows taskbar.

A menu and a tooltip associated with the system tray's Display Properties application icon
Figure 3. A menu and a tooltip associated with the system tray’s Display Properties application icon

Whenever the user needs to interact with one of these applications, the user makes an appropriate gesture with the mouse. For example, double-clicking the mouse’s left button while the mouse pointer is over the application’s icon typically causes the application’s main window to appear (on Windows platforms). Similarly, moving the mouse pointer over the application’s icon and clicking the mouse’s right button typically displays an application-specific pop-up menu.

Mustang introduces classes java.awt.SystemTray and java.awt.TrayIcon for interacting with the system tray: SystemTray represents the desktop’s system tray, and TrayIcon represents a tray icon that can be added to the system tray.

As with SplashScreen, you cannot create SystemTray objects. Instead, your application must call SystemTray‘s public static SystemTray getSystemTray() method to return the SystemTray instance that represents the system tray area. Because that method throws an UnsupportedOperationException if the underlying platform does not support a system tray, you must first call public static boolean isSupported(). This method returns true if at least minimal support for the system tray is provided (in addition to displaying the icon, minimal support includes either a pop-up menu, which displays when you right-click the icon, or an action event, which fires when you double-click the icon); false returns if there is no support for the system tray. The code fragment below shows the proper way to obtain the SystemTray instance:

if (SystemTray.isSupported ()) {

SystemTray tray = SystemTray.getSystemTray ();

// Do stuff with tray. }

Assuming that the system tray is supported, you can call the following seven methods to add various functionalities:

  • public void add(TrayIcon trayIcon): Adds a TrayIcon to the SystemTray, after which, the icon described by trayIcon becomes visible in the system tray. The order by which icons are added to the system tray depends on the platform implementation. Also, icons are automatically removed from the system tray when the application exits or when the system tray becomes unavailable. This method throws a NullPointerException if trayIcon is null, an IllegalArgumentException if you try to add the same TrayIcon instance more than once, and an AWTException if the system tray is not available.
  • public void addPropertyChangeListener(String propertyName, PropertyChangeListener listener): Adds a java.beans.PropertyChangeListener to the listener list for the trayIcons property, which must be propertyName‘s value. The listener is invoked whenever this application adds a TrayIcon to or removes a TrayIcon from the SystemTray—or when icons are automatically removed. No exception is thrown, and nothing happens if propertyName or listener is null.
  • public PropertyChangeListener [] getPropertyChangeListeners(String propertyName): Returns an array of PropertyChangeListeners (for the current application) associated with the named property. Only trayIcons is currently supported and must be specified as propertyName‘s value. If you pass null or any other value, an empty array returns.
  • public TrayIcon [] getTrayIcons(): Returns an array of all TrayIcons added to the SystemTray by the current application. The returned array is a copy of the actual array and can be modified without affecting the system tray’s icons. If no TrayIcons have been added, this method returns an empty array.
  • public Dimension getTrayIconSize(): Returns, as a java.awt.Dimension object, the horizontal and vertical size, in pixels, that an icon occupies when it appears in the system tray. Call this method to determine the preferred size of an icon before creating that icon. This method is a convenience method for TrayIcon‘s public Dimension getSize() method.
  • public void remove(TrayIcon trayIcon): Removes the specified TrayIcon from the SystemTray. The icon is removed from the system tray, and any registered property change listeners are notified. No exception is thrown, and nothing happens if trayIcon is null.
  • public void removePropertyChangeListener(String propertyName, PropertyChangeListener listener): Removes listener for the property identified by propertyName, which must be trayIcons (otherwise, there is no point in calling this method). No exception is thrown, and nothing happens if propertyName or listener is null.

Listing 4 describes a SystemTrayDemo1 application that demonstrates the methods described above. This application creates a filled yellow circle centered within a filled red rectangle, adds that icon to the system tray, pauses for three seconds, removes the added icon from the system tray, pauses for another three seconds, and terminates.

Listing 4. SystemTrayDemo1.java

// SystemTrayDemo1.java

import java.awt.*; import java.awt.image.*; import java.beans.*;

public class SystemTrayDemo1 { public static void main (String [] args) { if (SystemTray.isSupported ()) { // Acquire the system tray.

SystemTray tray = SystemTray.getSystemTray ();

// Register a property change listener to report icon additions and // removals from the system tray.

PropertyChangeListener pcl; pcl = new PropertyChangeListener () { public void propertyChange (PropertyChangeEvent pce) { System.out.println ("Property changed = " + pce.getPropertyName ()); System.out.println ();

TrayIcon [] tia = (TrayIcon []) pce.getOldValue (); if (tia != null) { System.out.println ("Old tray icon array contents: "); for (int i = 0; i < tia.length; i++) System.out.println (tia [i]); System.out.println (); }

tia = (TrayIcon []) pce.getNewValue (); if (tia != null) { System.out.println ("New tray icon array contents: "); for (int i = 0; i < tia.length; i++) System.out.println (tia [i]); System.out.println (); } } }; tray.addPropertyChangeListener ("trayIcons", pcl);

// Create an image for the icon.

Dimension size = tray.getTrayIconSize (); BufferedImage bi = new BufferedImage (size.width, size.height, BufferedImage.TYPE_INT_RGB); Graphics g = bi.getGraphics ();

g.setColor (Color.red); g.fillRect (0, 0, size.width, size.height); g.setColor (Color.yellow); int ovalSize = (size.width < size.height) ? size.width : size.height; ovalSize /= 2; g.fillOval (size.width/4, size.height/4, ovalSize, ovalSize);

// Create an icon from this image and add that icon to the system // tray. Terminate program if icon cannot be added.

TrayIcon icon = null; try { tray.add (icon = new TrayIcon (bi)); } catch (AWTException e) { System.out.println (e.getMessage ()); return; }

// Pause to observe icon in system tray.

try { Thread.sleep (3000); } catch (InterruptedException e) { }

// Remove icon from system tray.

tray.remove (icon);

// Pause to observe system tray minus the icon.

try { Thread.sleep (3000); } catch (InterruptedException e) { }

// Terminate this application.

System.exit (0); } } }

After acquiring the system tray instance, registering a property change listener with that instance, and creating a java.awt.Image for the icon, Listing 4 creates a TrayIcon object based on this icon and then adds that object to the SystemTray. The object is created by calling TrayIcon‘s public TrayIcon(Image image) constructor. That constructor stores image in the TrayIcon object. Later, when tray.add (icon = new TrayIcon (bi)); adds the TrayIcon to the SystemTray, that image is extracted and its contents displayed in the system tray. This constructor and TrayIcon‘s two other constructors each throw an IllegalArgumentException if image is null; the constructors throw an UnsupportedOperationException if the current platform does not support a system tray.

When you run SystemTrayDemo1 at the command line, you will discover a new icon in the system tray and output similar to the following in the console window:

Property changed = trayIcons

Old tray icon array contents:

New tray icon array contents: java.awt.TrayIcon@199f91c

Property changed = trayIcons

Old tray icon array contents: java.awt.TrayIcon@199f91c

New tray icon array contents:

After a few seconds, the icon disappears and SystemTrayDemo1 terminates, which is not appropriate for applications that run continuously. Instead, you will want the application to terminate in response to the user selecting an appropriate menu item from the pop-up menu that appears when the user right-clicks the application’s icon. You can achieve this behavior as follows: create an instance of the java.awt.PopupMenu class, create an appropriate menu item with an attached action listener that terminates the application when this menu item is selected, add the menu item to the pop-up menu, and associate the pop-up menu with the icon by invoking the public TrayIcon(Image image, String tooltip, PopupMenu popup) constructor. Along with the icon’s Image and associated PopupMenu, you specify a String that identifies the tooltip text that appears whenever the mouse pointer moves over the icon. Passing null for tooltip prevents a tooltip from displaying. The following code fragment introduces an icon with its own tooltip and pop-up menu into the system tray:

PopupMenu popup = new PopupMenu (); MenuItem miExit = new MenuItem ("Exit"); ActionListener al; al = new ActionListener () { public void actionPerformed (ActionEvent e) { System.out.println ("Goodbye"); System.exit (0); } }; miExit.addActionListener (al); popup.add (miExit);

TrayIcon ti = new TrayIcon (bi, "System Tray Demo #2", popup);

tray.add (ti); // Assume that tray was previously created.

The pop-up menu appears when the user right-clicks the icon. The user can then select the menu’s Exit menu item to execute that menu item’s action listener, which terminates the application.

In addition to this right-click mouse gesture, you might want to take action when the user performs another kind of mouse gesture on the icon, such as double-clicking it. To let applications respond to these mouse gestures, TrayIcon provides the following listener-registration methods:

  • public void addActionListener(ActionListener listener): Adds an action listener to this TrayIcon. This listener‘s public void actionPerformed(ActionEvent e) method is invoked whenever the user double-clicks the icon.

    Because you might want to share a single action listener among multiple TrayIcons, it will be important to determine which icon is responsible for firing an action event and invoking the listener. You can simplify this task by assigning a unique command to each TrayIcon via its public void setActionCommand(String command) method. You can then invoke, from within the listener, java.awt.event.ActionEvent‘s public String getActionCommand() method to return the command name and identify the TrayIcon. For convenience, TrayIcon also specifies a public String getActionCommand() method.

    Invoke TrayIcon‘s public void removeActionListener(ActionListener listener) method to remove listener from this TrayIcon. You can also obtain an array of all registered action listeners by invoking public ActionListener [] getActionListeners().

  • public void addMouseListener(MouseListener listener): Adds a mouse listener to this TrayIcon. This listener‘s various methods (except public void mouseEntered(MouseEvent e) and public void mouseExited(MouseEvent e), which are not supported) are invoked whenever the user presses, releases, or clicks the mouse’s left button while the mouse pointer hovers over the icon.

    If you call java.awt.event.MouseEvent‘s inherited public Component getComponent() method, you receive the null reference. However, MouseEvent‘s inherited public Object getSource() method returns the TrayIcon associated with the event.

    The mouse coordinates can be retrieved by calling MouseEvent‘s public int getX() and public int getY() methods. Those coordinates are relative to the screen—not the TrayIcon.

    Invoke TrayIcon‘s public void removeMouseListener(MouseListener listener) method to remove listener from this TrayIcon. You can also obtain an array of all registered mouse listeners by invoking public MouseListener [] getMouseListeners().

  • public void addMouseMotionListener(MouseMotionListener listener): Adds a mouse motion listener to this TrayIcon. This listener‘s public void mouseMoved(MouseEvent e) method is invoked whenever the user moves the mouse pointer over the icon. (The public void mouseDragged(MouseEvent e) method is not supported.)

    Once again, getComponent() returns null, getSource() returns the TrayIcon associated with the event, and the coordinates returned by getX() and getY() are relative to the screen—not the TrayIcon.

    Invoke TrayIcon‘s public void removeMouseMotionListener(MouseMotionListener listener) method to remove listener from this TrayIcon. You can also obtain an array of all registered mouse motion listeners by invoking public MouseMotionListener [] getMouseMotionListeners().

Listing 5 describes a SystemTrayDemo2 application that creates a TrayIcon with its own String tooltip and PopupMenu. That application also calls the previous listener-registration methods to register listeners for responding to action, mouse, and mouse motion events.

Listing 5. SystemTrayDemo2.java

// SystemTrayDemo2.java

import java.awt.*; import java.awt.event.*; import java.awt.image.*; import java.beans.*;

public class SystemTrayDemo2 { public static void main (String [] args) { if (SystemTray.isSupported ()) { // Acquire the system tray.

SystemTray tray = SystemTray.getSystemTray ();

// Create an image for the icon.

Dimension size = tray.getTrayIconSize (); BufferedImage bi = new BufferedImage (size.width, size.height, BufferedImage.TYPE_INT_RGB); Graphics g = bi.getGraphics ();

g.setColor (Color.red); g.fillRect (0, 0, size.width, size.height); g.setColor (Color.yellow); int ovalSize = (size.width < size.height) ? size.width : size.height; ovalSize /= 2; g.fillOval (size.width/4, size.height/4, ovalSize, ovalSize);

try { // Create a popup menu that associates with this application's // icon. Selecting the menu's solitary menu item terminates the // application.

PopupMenu popup = new PopupMenu (); MenuItem miExit = new MenuItem ("Exit"); ActionListener al; al = new ActionListener () { public void actionPerformed (ActionEvent e) { System.out.println ("Goodbye"); System.exit (0); } }; miExit.addActionListener (al); popup.add (miExit);

// Create an icon from its image, select a tooltip to appear // when the mouse pointer hovers over the icon, and assign the // popup menu to the icon.

TrayIcon ti = new TrayIcon (bi, "System Tray Demo #2", popup);

// Create and attach an action listener to the icon. When you // invoke the appropriate gesture (such as double-clicking the // icon under Windows), the actionPerformed() method is called.

al = new ActionListener () { public void actionPerformed (ActionEvent e) { System.out.println (e.getActionCommand ()); }

}; ti.setActionCommand ("My Icon"); ti.addActionListener (al);

// Create and attach a mouse listener to record mouse events that // involve this icon.

MouseListener ml; ml = new MouseListener () { public void mouseClicked (MouseEvent e) { System.out.println ("Tray icon: Mouse clicked"); }

public void mouseEntered (MouseEvent e) { System.out.println ("Tray icon: Mouse entered"); }

public void mouseExited (MouseEvent e) { System.out.println ("Tray icon: Mouse exited"); }

public void mousePressed (MouseEvent e) { System.out.println ("Tray icon: Mouse pressed"); }

public void mouseReleased (MouseEvent e) { System.out.println ("Tray icon: Mouse released"); } }; ti.addMouseListener (ml);

// Create and attach a mouse motion listener to record mouse // motion events that involve this icon.

MouseMotionListener mml; mml = new MouseMotionListener () { public void mouseDragged (MouseEvent e) { System.out.println ("Tray icon: Mouse dragged"); }

public void mouseMoved (MouseEvent e) { System.out.println ("Tray icon: Mouse moved"); } }; ti.addMouseMotionListener (mml);

// Add icon to system tray.

tray.add (ti); } catch (AWTException e) { System.out.println (e.getMessage ()); return; } } } }

As with SystemTrayDemo1, SystemTrayDemo2 displays the same icon in the system tray. Move the mouse pointer over this icon and its tooltip will appear, in addition to “Mouse moved” messages that appear in the console window. Try single-clicking the mouse’s left button while the mouse pointer hovers over this icon; various pressed/released/clicked messages appear in the console window. If you double-click the icon, the name of the action command for the solitary TrayIcon will display in the console window. Finally, right-click the icon and you will see a pop-up menu with an Exit menu item. Figure 4 presents two views of the system tray with SystemTrayDemo2‘s icon. In the left view, you see the tooltip over the icon. In the right view, you are presented with the icon’s pop-up menu.

The TrayIcon class provides additional methods—some of these methods are called by constructors—that you may wish to call. These methods include:

  • public void displayMessage(String caption, String text, TrayIcon.MessageType messageType): Displays a pop-up message near the icon in the system tray. The message displays for a platform-dependent amount of time and then disappears. (I believe this method does not affect the tooltip assigned to the TrayIcon.)

    The caption typically displays above the text, which is the message. Either caption or text may be null. If both are null, this method throws a NullPointerException. Finally, messageType is one of these message types: TrayIcon.MessageType.ERROR (error message), TrayIcon.MessageType.INFO (informational message), TrayIcon.MessageType.NONE (simple message), or TrayIcon.MessageType.WARNING (warning message). The platform can use the messageType value to determine what action—display a graphic or sound a noise—to perform while the message appears.

    This method is not supported on all platforms. For example, I could not get it to display a message on the Windows ME platform.

  • public void setImage(Image image): Establishes image as the Image for this TrayIcon. This is handy for indicating a change in application status. The previous Image is discarded without calling Image‘s flush() method—you must explicitly call that method to flush all resources (including pixel data cached for rendering to the screen) used by the previous Image object. A NullPointerException is thrown if image is null. Call the public Image getImage() method to return the current Image.
  • public void setImageAutoSize(boolean autosize): Sets the TrayIcon‘s auto-size property. This property determines whether the Image is automatically resized to fit the space allocated for the system tray icon. If you pass true to autosize, the Image shrinks or expands as necessary. Otherwise, the Image is cropped to fit the allocated space. Call the public boolean isImageAutoSize() method to return the value of the auto-size property.
  • public void setPopupMenu(PopupMenu popup): Sets the pop-up menu for this TrayIcon. If popup is null, no PopupMenu is associated with this TrayIcon. An IllegalArgumentException occurs if you attempt to set the same pop-up menu on multiple TrayIcons. Some platforms may not support a pop-up menu; they may either not display a menu or display a native version of the menu when the user right-clicks the icon. Call the public PopupMenu getPopupMenu() method to return the current PopupMenu.
  • public void setToolTip(String tooltip): Sets the tooltip for this TrayIcon. This tooltip displays when the user moves the mouse over the icon in the system tray. To remove the tooltip, pass null as the value of tooltip. The tooltip may be truncated on some platforms. Call the public String getToolTip() method to return the current tooltip String.

In closing, I would like to ward off a potential point of confusion. The SystemTray documentation for the isSupported() method recommends that you “add the default action to both the action listener and the pop-up menu” to ensure that the system tray icon’s default action is always accessible. What does this mean? Simply add a “Default” menu item to the PopupMenu that has the same action listener as the action listener you attach to the TrayIcon, as the code fragment below demonstrates:

PopupMenu popup = new PopupMenu ();

MenuItem miDefault = new MenuItem ("Default"); ActionListener alDefault; alDefault = new ActionListener () { public void actionPerformed (ActionEvent e)

{ System.out.println (e.getActionCommand ()); } }; miDefault.addActionListener (alDefault); popup.add (miDefault);

MenuItem miExit = new MenuItem ("Exit"); ActionListener alExit; alExit = new ActionListener () { public void actionPerformed (ActionEvent e) { System.out.println ("Goodbye"); System.exit (0); } }; miExit.addActionListener (alExit); popup.add (miExit);

TrayIcon ti = new TrayIcon (bi, "System Tray Demo #2", popup); ti.addActionListener (alDefault);

Conclusion

Mustang is coming. You’ll want to acquaint yourself with this Java platform before its official release later this year. To help you get started, this article has presented a small sample of what you will find in that platform. The article focused on four important new features: console I/O, partition-space methods, an API for working with splash screens, and an API for interacting with the system tray.

Jeff Friesen is a freelance software developer and educator specializing in C, C++, and Java technology.