Sun Java Solaris Communities My SDN Account Join SDN
 
Article

RMI

 
 

Articles Index


Java RMI (remote method invocation) technology makes it very easy to create web-based applets that perform powerful server-side operations, such as accessing a database or communicating with remote server applications. However, a number of RMI limitations make its use in Internet-based applets impractical. This article describes a way to work around these limitations by using Java servlets and object serialization for applet/server communication, and demonstrates several ways to use this technique. The focus of this article is on converting an existing application, but the same design can used when creating new applications.

The Benefits of Using RMI in Applets

In theory, choosing RMI as a means of communication between Java applets and server applications on the Internet is a great idea. The benefits of using RMI in such an application are persuasive, for the following reasons:

  • RMI is part of the core Java 1.1 specification, as well as Java 2 Standard Edition. This means that your RMI applet will be supported by every JDK 1.1 and 1.2-compatible client, so you don't have to worry about creating your own communications classes and packaging them with your applet.
  • Included in standard Java 1.1 and 1.2 runtime packages (JDK and JRE) is an executable version of the RMI registry, the server responsible for providing access to RMI objects. This allows you to deploy RMI-based server applications without needing an expensive application server or other add-on for the server side.
  • Although RMI typically makes a direct connection between applet and server, it will also tunnel its transaction through HTTP if necessary. This allows users who are connected to the Internet through an HTTP proxy server to use RMI applets.
  • RMI allows your applet code to execute server-side operations with simple function call syntax, so it's incredibly simple to write the code for such a distributed application.

The Limitations of RMI Applets on the Internet

Although using RMI in Internet-based applets seems like a good idea, in practice, the instability of Internet connections and a lack of client support make it impractical. Among the obstacles to deploying RMI applets on the Internet are the following:

  • Despite supporting the majority of the Java 1.1 specification, the most popular web browsers -- Netscape Navigator and Microsoft Internet Explorer -- do not adequately support RMI. Navigator supports RMI on most platforms but does not support HTTP tunneling, so Netscape users connected to the web through a proxy server can't use an RMI applet. Meanwhile, Netscape for the Macintosh does not support RMI at all. Internet Explorer 4.0-5.0 also lacks RMI support, although a patch is supposedly available to add the missing classes (the RMI classes are available from Microsoft's web site, but instructions for installing them is not provided). As a result, a large percentage of Internet surfers can't use RMI applets in their current browser.
  • The RMI registry can experience problems when handling connections via the Internet. A bug that causes the registry to stop accepting new connections occurs all too frequently with web-based RMI clients, requiring that the registry be shut down and restarted. Also, in Java runtime environments prior to JDK 1.2.2, server sockets created to handle connections from Internet-based RMI clients can live on indefinitely (consuming server resources) even after those clients are shut down.
  • The RMI registry is not as flexible as most server applications, answering only to client connections made to one specific hostname. This means that your RMI server application cannot support multiple domains on a single web server, which limits your deployment options.
  • Many applications may require that data exchanged between client and server be encrypted, compressed, or handled in some other fashion. Doing this with RMI is simple enough, requiring only that you implement your own socket classes and an RMI socket factory that creates your special sockets. However, overriding the default socket factory disables RMI's ability to do HTTP tunneling, thereby preventing access by proxy server users.
  • Deploying an RMI server application requires additional network firewall configuration. By default, RMI objects are bound to random, "anonymous" server port numbers, making it impossible to predict the ports that must be available to Internet clients. RMI functionality new to Java 2 allows you to specify a port number when creating a remote object, but deployment still requires that firewalls allow access to ports not typically used by web-based applications.

Sample Application

In this article you'll be working with a sample application: a simple web-based database client. The application consists of a Java applet that displays database values, and a server-side Java object that performs database tasks (add, remove, modify, and retrieve data) on behalf of the applet. Communication between the applet and the server object is via RMI.

The following graphic illustrates the original architecture of the application:

The server side of this illustration is a Java application that does three things: it creates an RMI registry, creates an instance of the database access object (UserDatabaseImpl), and binds the object to the registry to make it available to remote clients. Here are the lines of code from the server application that accomplish these tasks:

// Create the registry on port 1099
Registry reg = 
 java.rmi.registry.LocateRegistry.createRegistry(
 1099);
// Create the database object
UserDatabaseImpl db = new UserDatabaseImpl();
// Bind the object to the registry as "DB1"
java.rmi.Naming.rebind("DB1", db);

Once the database object is bound to the registry, your client applet can use java.rmi.Naming.lookup() to get a reference to it. Here's the client code:


UserDatabase db; //The remote database object
...
String host = this.getCodeBase().getHost();
String name = "//" + host + "/DB1";
db = (UserDatabase)java.rmi.Naming.lookup(name);

Once the applet has a reference to the remote object, it can execute database operations by invoking methods on this object. As with other RMI objects, your database object (UserDatabaseImpl) implements a remote interface (UserDatabase) that extends java.rmi.Remote. This remote interface defines the methods of the database object that can be executed by the client applet, and looks like this:
public interface UserDatabase 
 extends java.rmi.Remote 
{
	public String createUser(
	 UserData data) throws RemoteException;
	public void editUser(
	 String userid, UserData data) 
	  throws RemoteException;
	public void deleteUser(
	 String userid) throws RemoteException;
	public UserData getUser(
	 String userid) throws RemoteException;
	public Hashtable listUsers(
	 ) throws RemoteException;
}

To execute a database operation the client applet simply calls one of these methods on the remote object. For example, to get a list of the users currently in the database, the client applet executes:

Hashtable users = db.listUsers();

Overview of Changes

Although the above code snippets demonstrate the ease with which RMI allows you to create applets that access your database and perform other server-side tasks, you still want to eliminate it from the client side of your application because of the issues described above. In doing so, you have the following goals:

  • Make all transactions object-based. In other words, to accomplish a task you want to send whole objects to the server and receive one or more objects in return. RMI allows you to easily do this via function calls, and in your changes you want to maintain this object-oriented exchange.
  • Use HTTP for all applet/server communications. By using only HTTP, you can make your applet as compatible as possible with proxy servers and network firewalls.
  • Make zero changes to your RMI server application. Instead of rewriting your server code you just want to add a servlet layer to facilitate applet communication, so you shouldn't have to touch the server objects that you've already implemented.
  • Write a bare minimum of new code. Again, you don't want to do any more work than you have to.

To accomplish these goals you'll add a series of Java servlets on the server side that will execute RMI transactions on behalf of the client applet. To communicate with these servlets via HTTP, you'll create a proxy object on the client side whose methods will mirror the methods of the RMI server object. Where the applet currently executes methods directly on the RMI object, it will instead execute methods on the proxy object, thereby preserving all of the function syntax in your applet code.

Once you complete the changes your application's architecture will look like this:

The Servlets

The servlet layer of your new architecture will contain a series of servlets that each executes a single method on your remote object. Here are the servlets you'll be making and the method of UserDatabase they are associated with:

Servlet NameUserDatabase Method
UserCreateServletcreateUser()
UserEditServleteditUser()
UserDeleteServletdeleteUser()
UserGetServletgetUser()
UserListServletlistUsers()

Because your servlets will each execute a single RMI call, they will be extremely simple. Each servlet will perform the same basic tasks:

  1. Get a reference to the database object from the RMI registry.
  2. Read in parameter values from the client.
  3. Execute the appropriate method on the database object.
  4. Write the return value to the applet.

The first of these steps—getting the database object from the registry— will be common to all of your servlets, so you'll define it once in a super-class servlet and have all of your method-specific servlets extend it. In defining this parent servlet you want to provide flexibility by making the location of the RMI registry configurable via an initialization parameter. This allows you to locate the registry on a system other than the web server (possibly one behind a firewall), which you couldn't do when the applet required access to the registry. Once you have the location of the registry you can get the database object via java.rmi.Naming.lookup() just as the original applet code does.

Here then is the code for DatabaseServlet, your servlet super-class:

public class DatabaseServlet extends HttpServlet
{
   String registry; // The path to the RMI 
   //registry
   UserDatabase db; // The database access 
   //object

    public void init(
     ServletConfig config)
       throws ServletException
	{
		super.init(config);
		// Get the location of 
		//the RMI registry.
		registry = config.getInitParameter(
		  "registry");
		// If undefined, assume the local host
		if (registry == null) registry = 
		  "//localhost";
		try {
		  // Now get the database object
		 String name = registry + "/DB1";
		 db = (UserDatabase)Naming.lookup(
		  name);
		 } catch (Exception e) {
			e.printStackTrace();
		} 
	}
}

Now that you have your parent servlet you can create the individual servlets that extend this one. In doing so, you'll follow the recipe given above: read in parameter values, execute the database method, and return the result. All of these operations will occur in the servlet's doPost() method, which will also include the code required to open and close the I/O streams to the applet.

Here's your first servlet, UserCreateServlet, which executes the createUser() database method on behalf of the applet:

public class UserCreateServlet 
 extends DatabaseServlet
{
	public void doPost( 
	  HttpServletRequest req, 
	    HttpServletResponse resp)
	throws ServletException, IOException
	{
	// Open the I/O streams
	ObjectInputStream in = new 
	 ObjectInputStream(
	  req.getInputStream() );
		ObjectOutputStream out = 
		 new ObjectOutputStream( 
		  resp.getOutputStream() );

		try {

	// 1. Read in parameters from 
	//the applet 
	      UserData data = (
		 UserData)in.readObject();

	// 2. Execute the RMI call
	String id = db.createUser(data);

	// 3. Write the result
	out.writeObject(id);

	} catch (
	 Exception e) { e.printStackTrace(
	  ); }

	// Close the I/O streams
	in.close();
	out.close();

	}
}

A couple of notes on this code:

  • You're using ObjectInputStream and ObjectOutputStream here because you want to maintain object-based transactions between the applet and server. In this servlet you could have returned the value of the result String instead of the entire String object, but sticking with an objects in/objects out approach makes your lives much simpler.
  • Although you didn't declare or initialize the database object (db) in the code for UserCreateServlet, you don't need to. Again, this servlet extends DatabaseServlet, the parent servlet you defined above, so it gets a reference to the database object from the RMI registry when the servlet is initialized.

In defining your other servlets you can simply copy and paste the contents of UserCreateServlet. The only code that needs to change for each servlet (besides the class name, of course) is the three lines of code in the try/catch block— those are the only method-specific lines. So to create your UserGetServlet servlet, which executes the database's getUser() method, you simply change these lines to be:

	// 1. Read in parameters from 
	//the applet 
	String id = (String)in.readObject();

	// 2. Execute the RMI call
	UserData data = db.getUser(id);

	// 3. Write the result
	out.writeObject(data);
	

In both of these servlets you've read in one object and written out one object, but you can read or write as many objects as you want. In the case of the database's editUser() method, you need to retrieve two parameters from the applet while returning none. In your applet that executes this method (UserEditServlet) you start with the same three-line template and simply add an extra readObject() line to get the second parameter, and remove the writeObject() line that writes the return value:


  // 1. Read in parameters from 
  //the applet 
   String id = (String)in.readObject();
   UserData data = (
    UserData)in.readObject();

  // 2. Execute the RMI call
   db.editUser(id, data);

  // 3. Write the result
   // [no return value]
	

Your other servlets all follow the same format, so you won't print them all here Download code

Client Proxy Object

Now that you have servlets that can execute your RMI methods via HTTP, your client applet needs to know how to use these servlets. Although you could rewrite every RMI method call in the applet to include the corresponding servlet calls, that could be a lot of changes to existing code. That's why you'll use a proxy object that duplicates the database object's syntax and does all of the necessary servlet communication; this lets you keep almost all of your current applet code in tact.

In creating your proxy object, UserDatabaseProxy, you start with the UserDatabase interface that defines the available remote methods. The methods of your proxy object should be identical to the interface, which defines these methods:

 String createUser(UserData data) 
 void editUser(
  String userid, UserData data) 
 void deleteUser(String userid) 
 UserData getUser(String userid) 
 Hashtable listUsers() 
	

As you did when creating your servlets, you'll following a simple recipe when defining each of these methods in your proxy object. In each of these methods you want to accomplish the following: Open a connection to the target servlet, write one or more parameters to the servlet, and read the result of the servlet execution and return it to the applet.

To help you accomplish this you'll be using a helper class called ServletWriter. This class manages the I/O streams between applet and servlet and executes the actual HTTP POST transaction. Like your servlets, ServletWriter uses ObjectInputStream and ObjectOutputStream to pass whole objects between client and server. The lone method defined by ServletWriter is:

ObjectInputStream postObjects(URL servlet, Serializable objs[]) where servlet defines the URL of the target servlet, and objs defines a list of Serializable objects to be written to the servlet. The method returns the ObjectInputStream from which your methods can retrieve return values from the servlet. See ServletWriter.java code for details. With ServletWriter at your disposal, here's what each of your proxy object methods will do:

  1. Create a URL for the target servlet.
  2. Create an array of Serializable objects that contains the method parameters.
  3. Call ServletWriter.postObjects() to execute the servlet transaction.
  4. Read and return the result value.

Step 1 here requires that you know the base URL for the servlet web server. Since this value will be the same for each method, you'll define it once by setting it in the constructor of your proxy object:

URL webBase;// The URL to the servlet web server 

public UserDatabaseProxy(URL web) 
{
	webBase = web;
}

Because the URL to the servlet web server is initialized during construction, each of your methods can create the URL to their corresponding servlets relative to this base URL.

With that out of the way, you can now write each of the proxy object's methods. You'll start with createUser(), which posts data to UserCreateServlet:


public String createUser(
 UserData data) throws Exception 
{
// 1. Create a URL for the target 
//servlet
URL servlet = new URL(
  webBase,
    "servlet/UserCreateServlet");

// 2. Create an array of parameter 
//objects
Serializable objs[] = { data };

// 3. Execute the servlet 
//transaction
ObjectInputStream in = 
 ServletWriter.postObjects(
  servlet, objs);

// 4. Read and return the result value
 String id = (String)in.readObject();
 in.close();
 return id;
}

As with your servlets, creating other methods in your proxy object requires mostly copying and pasting this first method. Here is the code for the getUser() method, which differs from the above method only in the name of the target servlet (UserGetServlet) and the type of the return object:


public UserData getUser(
 String userid) throws Exception 
{
// 1. Create a URL for the target servlet
URL servlet = new URL(webBase, 
  "servlet/UserGetServlet");

// 2. Create an array of parameter objects
	Serializable objs[] = { userid };

// 3. Execute the servlet transaction
ObjectInputStream in = 
  ServletWriter.postObjects(
  servlet, objs);

// 4. Read and return the result value
UserData data = (UserData)in.readObject();
in.close();
return data;
}

The other methods of your proxy object are similarly constructed, so you won't list them all here. UserDatabaseProxy.java

Applet Changes

Once you have your proxy object and servlets in place, all you need to do is update your applet to use of the proxy object instead of the original RMI object. Here again is the original code for accessing the database:


UserDatabase db; //The remote database object
...
String host = this.getCodeBase().getHost();
String name = "//" + host + "/DB1";
db = (UserDatabase)Naming.lookup(name);
	

Besides redefining db to be an instance of the proxy object instead of the RMI object, you need to create the URL for the servlet web server (required by UserDatabaseProxy's constructor). To do this you construct a URL from the protocol, host name, and port number of the applet's code base:


UserDatabaseProxy db; // The proxy 
//database object
	

URL codebase = this.getCodeBase();

// Get the host, protocol, and port
String host = codebase.getHost();
String protocol = codebase.getProtocol();
int port = codebase.getPort();

// Build the URL for the servlet 
//web server
URL servletBase = new URL(
   protocol + "://" + host + ":" + port);

// Create the proxy object
db = new UserDatabaseProxy(servletBase);

With the original RMI code replaced with this new proxy object code, you're basically done with your applet changes—no other modifications are necessary, since the function call syntax on db the proxy object is the same as it was for db the RMI object. You cheated here a little by glossing over exception handling, since your remote method calls throw RemoteException while your proxy object methods may throw a variety of exceptions (MalformedURLException, ClassCastException, etc.), but you get the idea.

That's it—you're done. Your client applet can now use HTTP instead of RMI to communicate with the database access object on the server end, and you didn't even have to change a single line of your existing server code (and only a half dozen lines in the applet). With just these few changes you've made your applet more compatible, your server more flexible, and your communication much more reliable and Internet-friendly.

Beyond the Basics

In modifying your sample application above you designed your servlets to mimic what your RMI object does—namely, they take some number of parameters and return a single object of a specific type (or none). But unlike RMI method calls, your servlets can be extremely flexible in the way that they handle and return data. The following examples illustrate a few of the options available for applet/servlet/RMI applications.

Receiving Multiple Objects From One Call

In your above changes you created a servlet that, like your RMI object, responds to an applet request by returning a Hashtable that may contain a large number of objects. That works, but as with the original RMI method call, the client will be left waiting for the entire Hashtable to be returned. This prevents your applet from displaying a progress bar or stopping the operation after only a subset of values have been retrieved, highly desirable features if a large number of entries are involved. (Of course, you can retrieve a list of objects one-at-a-time by making a separate server call for each one, but doing so on thousands of objects is impractical.)

Instead of waiting for your servlet to return an Hashtable, you can send the keys and values of that Hashtable as individual objects. To see how, here's the original doPost() method from UserListServlet, which retrieves the list of database users in your sample application:


public void doPost(HttpServletRequest req, 
   HttpServletResponse resp)
 throws ServletException, IOException
{
  ObjectInputStream in = 
    new ObjectInputStream(
    req.getInputStream() );
     ObjectOutputStream out = 
   new ObjectOutputStream( 
    resp.getOutputStream() );
   try {
		Hashtable database = 
		 db.listUsers();
		out.writeObject(database);
	} catch (
	 Exception e) { e.printStackTrace(
	   ); }
	// Close the I/O streams
	in.close();
	out.close();
}

Here's another version of the above servlet, but this one informs the client how many entries there are in the return value, and then sends the Hashtable one key and value at a time:


public void doPost(
 HttpServletRequest req,
    HttpServletResponse resp)
 throws ServletException, IOException
{
  ObjectInputStream in = 
  new ObjectInputStream(
  req.getInputStream() );
  ObjectOutputStream out = 
   new ObjectOutputStream( 
    resp.getOutputStream(
	  ) );
	try {
          Hashtable database = 
	   db.listUsers();

	// Tell the client how many 
	//entries are coming
	  int num = database.size();
	   Integer numInt = new Integer(
		num);
	   out.writeObject(numInt);
	
	// Write each key and 
	//value of 
      //the Hashtable
         Enumeration keys = 
         database.keys();
	    while ( 
	     keys.hasMoreElements(
	      ) ) 
		{	
	      String nextKey = 
	      keys.nextElement();
              UserData nextData = (
	      UserData)database.get(
	       nextKey);
	      out.writeObject(nextKey);
	      out.writeObject(nextData);
		}
	} catch (
	 Exception e) { e.printStackTrace(
	        ); }
	// Close the I/O streams
	in.close();
	out.close();
}

In this new version of the servlet you're sending exactly the same Hashtable data as the old version, but you're now letting the client know how much data is coming before you start sending it. This gives the client a lot more flexibility in dealing with this data.

Obviously, if you change your servlet like this you need to change your proxy object to deal these return values. Here's the original code from your sample application's proxy object:


public Hashtable listUsers(
 ) throws Exception 
{
     servlet = new URL(webBase, 
     "servlet/UserListServlet");
      Serializable objs[] = {};
      in = ServletWriter.postObjects(
      servlet, objs);
      Hashtable database = (
      Hashtable)in.readObject();
      in.close();
      return database;
}

Here's another version of the same function that retrieves data from the new servlet, and also displays its progress in the applet's UI:

public Hashtable listUsers(
 java.awt.Label label) throws Exception 
{
  servlet = new URL(
   webBase, "servlet/UserListServlet");
     Serializable objs[] = {};
     ObjectInputStream in = 
     ServletWriter.postObjects(
      servlet, objs);

    // Initialize the return value
    Hashtable database = new Hashtable(
     );

    // Get the number of data 
    //values coming
      Integer numInt = (
       Integer)in.readObject();
	int num = numInt.intValue();
	
	// Read each key/value pair and 
	//add to the Hashtable
	for (int x = 1; x <= num; x++)
	{
	  label.setText(
	  "Getting data (
	  " + x + "/" + num + ")...");
	  String nextKey = (
	   String)in.readObject();
	  UserData nextData = (
	   UserData) in.readObject();
	  database.put(
	   nextKey, nextData);
	}
	in.close();
	return database;
}

When the applet executes this version of listUsers(), the given label displays the total number of entries and the current number being read (for example, "Getting data (7/26)...").

Combining Servlets

Another change you can make to the sample application is to consolidate all of your servlets into a single servlet, with all of the RMI methods calls contained in a single doPost() method. This complicates the servlet because it will need to switch between several code blocks based on the operation to perform, but it simplifies deployment by eliminating the need for multiple servlets.

Here's a single servlet that implements all of the database method calls that your five servlets, using an integer code sent by the applet to determine the appropriate action:

public class UniversalServlet extends DatabaseServlet
{
  static final int LIST   = 0;
  static final int GET    = 1;
  static final int CREATE = 2;
  static final int EDIT   = 3;
  static final int DELETE = 4;

   public void doPost(
    HttpServletRequest req, 
       HttpServletResponse resp)
    throws ServletException, IOException
	{
	// Open the I/O streams
	ObjectInputStream in = 
	 new ObjectInputStream(
	 req.getInputStream() );
	 ObjectOutputStream out = 
	 new ObjectOutputStream( 
	 resp.getOutputStream(
	  ) );

   try {
// Find out which operation to perform
  Integer codeInt = (
      Integer)in.readObject();
	int code = 
	 codeInt.intValue();

    if (code == LIST) {
	Hashtable database = 
	db.listUsers();
	out.writeObject(database);
}
	else if (code == GET) {
	String id = (
	String)in.readObject();
	UserData data = 
	 db.getUser(id);
	out.writeObject(data);
}
	else if (
	 code == CREATE) {
	UserData data = (
	UserData)in.readObject();
	String id = 
	 db.createUser(
	 data);
	out.writeObject(id);
}
	else if (code == EDIT) {
	String id = (
	String)in.readObject();
	UserData data = (
	UserData)in.readObject();
	db.editUser(id, data); 
}
  else if (code == DELETE) {
   String id = (
   String)in.readObject();
db.deleteUser(id); 
  }
		
} catch (
 Exception e) { e.printStackTrace(
   ); }

// Close the I/O streams
in.close();
out.close();
	}
}

Notice in this new servlet code that parameters for each operation are read only after the integer code is read and you determine which code block to execute. Unlike your previous servlets, you can't read all parameters from the input stream at the beginning of this doPost() because you don't know the number and type of parameters to read until after you know which operation to execute. To use this servlet you need to change all methods of your proxy object accordingly, using an integer code to specify the operation to perform. For example, here's a version of the proxy object's createUser() method that uses the above UniversalServlet:

public String createUser(
 UserData data) throws Exception 
{
 URL servlet = new URL(
	  webBase,
 "servlet/UniversalServlet");
 Integer code = 
  new Integer(2); //CREATE
 Serializable objs[] = 
 { code, data };
 ObjectInputStream in = 
  ServletWriter.postObjects(
   servlet, objs);
 String id = (
 String)in.readObject();
 in.close();
   return id;
}

The only change you've made here is to create an Integer that corresponds to the operation being performed (in this case, creating a database entry) and add this Integer to the array of parameters posted to the servlet.

Handling Multiple Object Types

You can also construct your applet to accommodate different object types in response to the same servlet call. For example, if while retrieving database information your servlet encounters a RemoteException, you may want your applet to receive the exception. To do so, you can write the RemoteException object to the servlet's ObjectOutputStream instead of the return value you were expecting. Although this would cause a ClassCastException to occur in the proxy object when it attempted to read the return value, you can use instanceof to determine whether the servlet output the return value or an exception, as in this example:

public UserData getUser(
  String userid) throws Exception 
{
 URL servlet = new URL(webBase, 
 "servlet/UserGetServlet");
 Serializable objs[] = { userid };
 objectInputStream in = 
 ServletWriter.postObjects(
  servlet, objs);

// Read the value as an Object
Object obj = in.readObject();

// Is it an Exception?
if (obj instanceof Exception) {
  throw (Exception)obj;
  }
  else {
	UserData data = (
	UserData)obj;
	in.close();
	return data;
}
}

Here you're reading in the return value from the servlet as type Object and then using instanceof to determine whether it's an Exception. If it is, you cast it as an Exception and throw it to let the applet deal with it as necessary; otherwise, you assume the return value is a UserData object, cast it as such, and return it to the applet.

Conclusion

As you can see, your options in working with applet/servlet communication are far greater than when using RMI in your applet; the examples shown here are just a start. While inserting another layer in your application may take a little more work, the payoff is an applet that is more compatible, client/server communication that is more flexible, and an RMI server application that is more reliable.

Download Source Code

Coffecup Logo

About the Author

Scott McPherson Scott McPherson is the founder and President of Maczieg Software, a Silicon Valley-based Java software company. Scott and his company recently launched MochaMail Online the Internet's first Java-based free email service.

Oracle is reviewing the Sun product roadmap and will provide guidance to customers in accordance with Oracle's standard product communication policies. Any resulting features and timing of release of such features as determined by Oracle's review of roadmaps, are at the sole discretion of Oracle. All product roadmap information, whether communicated by Sun Microsystems or by Oracle, does not represent a commitment to deliver any material, code, or functionality, and should not be relied upon in making purchasing decisions. It is intended for information purposes only, and may not be incorporated into any contract.