ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


Mac OS X for Java Geeks

Create Desktop Applications with Java-Based Web Technologies

by Will Iverson, Mac OS X Conference speaker and author of Mac OS X for Java Geeks
09/17/2003

So, you've mastered web application development, only to stumble into the decline and fall of web-based dot-com mania. To your chagrin, you've discovered that servers and bandwidth are a lot more expensive when you have to pay for them yourself instead of relying on your VC-funded IT department.

But you've still got the itch. You still want to build (and maybe even sell) great software. If you're a relatively new developer, it's likely that your skill set is focused on web technologies--things like HTTP and HTML, cookies, URLs, and forms. The next step, therefore, is to learn to create software that anyone can use (not just web server administrators). Instead of throwing out your skill set, why not learn to marry the concept of user-installable desktop applications with familiar web technologies? This union will provide opportunities to build a new class of applications--browser-based applications uniquely suited for this new age of browsers, wireless laptops, and 802.11-enabled PDAs. Some of the most interesting and innovative software out there is cropping up in this format.

Examples of Browser-Based Desktop Applications

Let's look at some examples of desktop applications where the primary interface is through a web browser:

Mac OSX Conference

Author conference session
Create Desktop Applications with Java-Based Web Technologies

A close look at how a Java-based web application can be converted to a desktop application for use on both Mac OS X and Windows systems. Particular emphasis will be paid toward understanding how to build complete packages that are easy for an end-user to install, plus solutions for technical and installation problems.

O'Reilly Mac OS X Conference
October 27-30, 2003
Santa Clara, CA


You can download and install all of these applications onto your desktop computer, but you interact with them via your web browser. Variously, the tools above either expect to reside in a web server, bundle a server, or implement the server from scratch internally. Powerful, yes, but also exceedingly complicated.

Installation Expectations

This brings us to the heart of the problem we're addressing in this article: what does the (admittedly mythological) "typical user" want, and how can we create software for this user?

Developers or administrators working with a typical web application can be expected to perform the following tasks:

Many technically savvy users can also perform these tasks, but it's way too much to ask for the typical user. Generally speaking, a typical user expects an experience like this:

I've named my efforts on this webtop project "Canteen." I've created an archive called canteen.zip, which has all of the source and various Tomcat libraries you'll need (approximate size: 3MB). You can also try out the resulting application and installer by visiting www.cascadetg.com/canteen/install.htm, and you can follow future improvements by visiting www.cascadetg.com/canteen.

In the remainder of this article, I will provide detailed, step-by-step instructions for building a simple, point-and-shoot installer for a basic web application using the Apache Jakarta Tomcat, a popular Java-based web application server (typically used for building JSP-based applications). Your users won't have to worry about virtual machines, web servers, or databases. I will use a combination of free tools and various Apache-license projects to build a user-installable version of this web application.

Required Software

We'll be using the following materials to build this application and the installer:

As a blatant plug, I'll note that information on how to integrate Mac OS X Finder events into Java applications is provided in my book, Mac OS X for Java Geeks. You'll also find information there on how to "fix" the menu bar.

Easy to Use Is Hard to Build

Users expect an application to be easy to use. To create that experience takes a lot of work. When you have completed the steps in this article, however, a user will be able to go to a web site, run a simple installer, and have what feels like a native application installed. Running the application will be as easy as clicking on the application icon. Launching the application icon will automatically launch a browser pointing at the web application URL.

Traditional applications typically start with a main() method, and Java is no different for standard desktop applications. On Mac OS X, additional interfaces are provided for handling different Finder events, such as a command to open a specific document, but in this instance we'll just be focusing on the cross-platform standard main() entry point.

Mac OS X for Java Geeks

Related Reading

Mac OS X for Java Geeks
By Will Iverson

Web applications typically require a server to be installed first, and then they are dropped into the server (similar to how a plug-in works). The basic idea is that the web application is wrapped by the web container, which is managed by the system administrator. Tomcat, for example, provides some command-line scripts for starting and stopping the server. In this case, however, we need to provide a very simple graphical user interface with which the user can start and stop the server, with our own main() handling the start and stop of the web server.

The New main()

The first example we'll look at is our core application launch logic, as shown in Example 1. We start by creating a JFrame to hold the server user interface (onjava.ServerUI) and setting the status to Launching.

Example 1. TomcatWrapper source

package onjava;

import java.net.*;
import java.io.*;
import java.awt.*;

public class TomcatWrapper
{
   public static void main(String[] args)
   {
      try
      {
         ServerUI myFrame = new ServerUI();
         myFrame.setStatus("Launching...");
         myFrame.show();
            
         // This will create a reference to a file in the
         // current working directory, which is the path
         // where the application started (at least on
         // Win32 & Mac OS X)
         File baseDirectory = new File("");
            
         // This is the path to the application's base directory
         String path = baseDirectory.getAbsolutePath();
         String[] temp =
         {path};
            
         // Launches the server
         onjava.EmbeddedTomcat.main(temp);
            
         // Wait a second to be sure the server is ready
         Thread.sleep(1000);

         myFrame.setStatus("Server running.");

         // Launches web browser pointed to the application
         edu.stanford.ejalbert.BrowserLauncher.openURL("http://127.0.0.1:8080/");
      }
      catch (Exception e)
      {
         e.printStackTrace();
      }
   }
};

The next thing we do is create a new java.io.File, but we only pass in a blank String to the constructor. This is an interesting thing; it creates a default reference to the current working directory at the application launch. We save and rely on this directory as the default installation directory for the web server throughout the rest of the application's life cycle. While this behavior is not to my knowledge a formal part of the specification, it's the default behavior on both the Mac OS X and Win32 JVMs.

After the server is started, we wait briefly to ensure that the server has launched, and then use the code found at browserlauncher.sourceforge.net to launch the user's browser. We use the user's loopback address (127.0.0.1) and pick a port (in this case, 8080).

Those of you who are paying attention may notice one small problem with this example: the application in the example binds the web application to port 8080, which, unfortunately, tends to be a popular port. (It's an easy-to-remember variant on port 80, the default for HTTP.) Each IP address on a computer can listen on a single port at a time to provide a service, and a great many of those ports are already used. By examining the list at www.iana.org/assignments/port-numbers, one discovers that 8080 is explicitly defined as an HTTP Alternate; good to know, but not particularly useful for resolving conflicts.

With that in mind, it's worth pondering the future. There are a great many ports that could be used in the dynamic range (and indeed could be dynamically generated at application launch), but unfortunately, users wouldn't be able to rely on any bookmarks if the ports are dynamically created. In the end, you wind up with a situation much like that of managing three-letter document types or creator codes; most software will grab certain types by default, and some will register with some central authority, but you'll probably want to provide a workaround to allow users to specify alternatives in the (hopefully rare) case of a collision for a undefined port.

Wrapping the Server

The code shown in Example 2 dives more deeply into the actual mechanics of launching the server. Here we rely on code very similar to that provided by the Tomcat documentation and James Goodwill in his aforementioned article. A few changes are required to ensure cross-platform compatibility; notably, relying on the System.getProperty("file.separator") property for determining the file path separator character (instead of hard-coding a particular / or \ character). We also make the code in the onjava.EmbeddedTomcat.main() method a bit smarter about handling and automatically registering WAR files.

Example 2 EmbeddedTomcat source

package onjava;

import java.net.URL;
import java.io.File;
import org.apache.catalina.Connector;
import org.apache.catalina.Context;
import org.apache.catalina.Deployer;
import org.apache.catalina.Engine;
import org.apache.catalina.Host;
import org.apache.catalina.logger.SystemOutLogger;
import org.apache.catalina.startup.Embedded;
import org.apache.catalina.Container;

public class EmbeddedTomcat
{
   private String path = null;
   private Embedded embedded = null;
   private Host host = null;
   /**
    * Default Constructor
    *
    */
   public EmbeddedTomcat()
   {
        
   }
    
   /**
    * Basic Accessor setting the value of the context path
    *
    * @param path - the path
    */
   public void setPath(String path)
   {
     this.path = path;
   }
    
   /**
    * Basic Accessor returning the value of the context path
    *
    * @return - the context path
    */
   public String getPath()
   {
      return path;
   }
    
   private static String dirchar = System.getProperty("file.separator");
   
   /**
    * This method Starts the Tomcat server.
    */
   public void startTomcat() throws Exception
   {
      Engine engine = null;
      // Set the home directory
      System.setProperty("catalina.home", getPath());
       
      // Create an embedded server
      embedded = new Embedded();
      // print all log statements to standard error
      embedded.setDebug(0);
        
      // Create an engine
      engine = embedded.createEngine();
      engine.setDefaultHost("localhost");
       
      // Create a default virtual host
      host = embedded.createHost("localhost", getPath()
      + dirchar + "webapps");
      engine.addChild(host);
        
      // Create the ROOT context
      Context context = embedded.createContext("",
      getPath() + dirchar + "webapps" + dirchar + "ROOT");
      host.addChild(context);
        
      // Install the assembled container hierarchy
      embedded.addEngine(engine);
       
      // Assemble and install a default HTTP connector
      Connector connector =
      embedded.createConnector(null, 8080, false);
      embedded.addConnector(connector);
        
      // Start the embedded server
      embedded.start();
        
      embedded.setLogger(new SystemOutLogger());
      host.setLogger(new SystemOutLogger());
      context.setLogger(new SystemOutLogger());
   }
    
   /**
    * This method Stops the Tomcat server.
    */
   public void stopTomcat() throws Exception
   {
      // Stop the embedded server
      embedded.stop();
   }
    
   /**
    * Registers a WAR with the container.
    *
    * @param contextPath - the context path under which the
    *               application will be registered
    * @param warFile - the URL of the WAR to be
    * registered.
    */
   public void registerWAR(String contextPath, URL warFile)
   throws Exception
   {
       
      if ( contextPath == null )
      {
            
         throw new Exception("Invalid Path : " + contextPath);
      }
      if( contextPath.equals("/") )
      {
           
         contextPath = "";
      }
      if ( warFile == null )
      {
            
         throw new Exception("Invalid WAR : " + warFile);
      }
       
      Deployer deployer = (Deployer)host;
      Context context = deployer.findDeployedApp(contextPath);
        
      if (context != null)
      {
            
         throw new
         Exception("Context " + contextPath
         + " Already Exists!");
      }
      deployer.install(contextPath, warFile);
   }
    
   /**
    * Unregisters a WAR from the web server.
    *
    * @param contextPath - the context path to be removed
    */
   public void unregisterWAR(String contextPath)
   throws   Exception
   {
        
      Context context = host.map(contextPath);
      if ( context != null )
      {
           
         embedded.removeContext(context);
      }
      else
      {
           
         throw new
         Exception("Context does not exist for named path : "
         + contextPath);
      }
   }
    
   public static void main(String args[])
   {
      try
       {           
         tomcat = new EmbeddedTomcat();
         tomcat.setPath(args[0]);
         tomcat.startTomcat();
            
         System.out.println("Using path: " + args[0]);
          
         String[] wars = new File(args[0] + dirchar + "webapps").list();
         if(wars != null)
            for(int i = 0; i < wars.length ; i++ )
            {
                System.out.println(wars[i]);
                if(wars[i].endsWith(".war"))
                {
                    File temp = new File((new java.io.File(args[0] + "/webapps/" 
                                          + wars[i])).toString());
                    URL tempURL = temp.toURL();
                    System.out.println(temp);
                        
                    URL url = new URL("jar:" + tempURL.toString() + "!/");
                      
                    String context = "/" + wars[i].substring(0, wars[i].length() - 4);
                       
                    tomcat.registerWAR(context , url);
                }
            }
            
            
      }
      catch( Exception e )
      {
            
         e.printStackTrace();
      }
   }
    
   private static EmbeddedTomcat tomcat;
    
   public static void stopServer()
   {
      try
      {
         tomcat.stopTomcat();}
      catch(Exception e)
      {
         e.printStackTrace();
      }
   }
    
}

In the interest of completeness, Example 3 describes the actual Swing user interface needed to build the application. I'll be honest: I knocked this off in a few minutes using NetBeans 3.5.

Example 3. ServerUI source

package onjava;

import java.awt.event.KeyEvent;
import java.awt.Toolkit;
import javax.swing.KeyStroke;

public class ServerUI extends javax.swing.JFrame
{
    
   /** Creates new form ServerUI */
   public ServerUI()
   {
      initComponents();
   }
    
   public int preferredMetaKey = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
    
   private void initComponents()
   {
      statusLabel = new javax.swing.JLabel();
      menuBar1 = new javax.swing.JMenuBar();
      fileMenu1 = new javax.swing.JMenu();
      browserMenuItem = new javax.swing.JMenuItem();
      fileMenuSep1 = new javax.swing.JSeparator();
      quitMenuItem = new javax.swing.JMenuItem();

      getContentPane().setLayout(null);

      setTitle("Canteen");
      addWindowListener(new java.awt.event.WindowAdapter()
      {
         public void windowClosing(java.awt.event.WindowEvent evt)
         {
            exitForm(evt);
         }
      });

      statusLabel.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
      statusLabel.setText("Launching...");
      getContentPane().add(statusLabel);

      fileMenu1.setText("File");
      browserMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_B, preferredMetaKey));
      browserMenuItem.setMnemonic('K');
      browserMenuItem.setText("Launch Browser");
      browserMenuItem.addActionListener(new java.awt.event.ActionListener()
      {
         public void actionPerformed(java.awt.event.ActionEvent evt)
         {
            browserMenuItemActionPerformed(evt);
         }
      });

      fileMenu1.add(browserMenuItem);

      fileMenu1.add(fileMenuSep1);

      quitMenuItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q, preferredMetaKey));
      quitMenuItem.setMnemonic('K');
      quitMenuItem.setText("Quit");
      quitMenuItem.addActionListener(new java.awt.event.ActionListener()
      {
         public void actionPerformed(java.awt.event.ActionEvent evt)
         {
            quitMenuItemActionPerformed(evt);
         }
      });

      fileMenu1.add(quitMenuItem);

      menuBar1.add(fileMenu1);

      setJMenuBar(menuBar1);
        
      statusLabel.setLocation(5, 20);
      statusLabel.setSize(290, 25);
      this.setSize(300, 100);
   }
    
   private void browserMenuItemActionPerformed(java.awt.event.ActionEvent evt)
   {
      try
      {
         edu.stanford.ejalbert.BrowserLauncher.openURL("http://127.0.0.1:8080/"); }
      catch (Exception e)
      {
         e.printStackTrace();
      }
   }
    
   private void quitMenuItemActionPerformed(java.awt.event.ActionEvent evt)
   {
      exitForm(null);
   }
    
   /** Exit the Application */
   private void exitForm(java.awt.event.WindowEvent evt)
   {
      this.setStatus("Shutting down...");
      onjava.EmbeddedTomcat.stopServer();
      System.exit(0);
   }
    
   public void setStatus(String in)
   {
      statusLabel.setText(in);
        
   }
    
   public static void main(String args[])
   {
      new ServerUI().show();
   }
    
    
   private javax.swing.JMenuItem browserMenuItem;
   private javax.swing.JMenu fileMenu1;
   private javax.swing.JSeparator fileMenuSep1;
   private javax.swing.JMenuBar menuBar1;
   private javax.swing.JMenuItem quitMenuItem;
   private javax.swing.JLabel statusLabel;
   
}

As shown in Figures 1, 2, and 3, there's room for improvement, but this gets the job done. Purists will note that the menu bar is incorrect (again, information on how to solve that particular problem can be found in my book). You'll notice, however, that the example works on both Mac OS X and Windows XP.


Figure 1. Server UI on Mac OS X


Figure 2. Server UI menu on Mac OS X


Figure 3. Server UI on Windows XP

Launching the Server

So we've got a working application. It's now possible to launch the application from the command line with a simple command.

java -classpath :./lib/bootstrap.jar:./lib/ant.jar:./lib/catalina-
ant.jar:./lib/catalina.jar:./lib/commons-beanutils.jar:./lib/commons-
collections.jar:./lib/commons-digester.jar:./lib/commons-fileupload-1.0-
beta-1.jar:./lib/commons-logging-api.jar:./lib/commons-
logging.jar:./lib/commons-modeler.jar:./lib/jakarta-regexp-
1.2.jar:./lib/jasper-compiler.jar:./lib/jasper-runtime.jar:./lib/mx4j-
jmx.jar:./lib/mx4j.license:./lib/naming-common.jar:./lib/naming-
factory.jar:./lib/naming-resources.jar:./lib/servlet.jar:./lib/servlets-
cgi.renametojar:./lib/servlets-common.jar:./lib/servlets-
default.jar:./lib/servlets-invoker.jar:./lib/servlets-
manager.jar:./lib/servlets-ssi.renametojar:./lib/servlets-
webdav.jar:./lib/tomcat-coyote.jar:./lib/tomcat-http11.jar:./lib/tomcat-
jk.jar:./lib/tomcat-jk2.jar:./lib/tomcat-util.jar:./lib/tomcat-warp.jar:. 
onjava.TomcatWrapper

OK, so that's a bit of a joke. Go ahead, send this to a customer and see how it flies. Unless your customer is a Java developer, you'll going to get some pretty strange looks.

It's a lot more reasonable to package these files up into a simple installer for the user. To accomplish this, we'll turn to the free version of the ZeroG InstallAnywhere product.

Note: One thing I've done here is put all of the various JAR files into a single lib directory, which means that some of the typical Tomcat CLASSPATH gradations are thrown out; for our purposes, that's fine. For "real-world" usage, it may make sense to merge these JARs as an integrated "platform" release, perhaps using Ant to crack and then rebuild a single merged JAR file. Or even better, build a system for managing and tracking the latest release versions (although that does get us closer to emulating the "DLL Hell" problem one sees on Windows).

Building the Installer

After downloading, installing, and launching, you'll be presented with a wizard for setting up your installer, as shown in Figure 4.


Figure 4. InstallAnywhere Now! New Project

Name your project, as shown in Figure 5.


Figure 5. Product Name

Add your various supporting files, as shown in Figure 6.


Figure 6. Adding Files

You'll then be prompted to set the application's main() class. InstallAnywhere will auto-detect the classes with main() methods (as shown in Figure 7). We're going to point InstallAnywhere at onjava.TomcatWrapper for our application, which you can enter manually.


Figure 7. Finding main()

In the next panel, you'll set the CLASSPATH for the application, as shown in Figure 8. You'll note that I've deselected the WAR file.


Figure 8. Setting the CLASSPATH

Finally, in Figure 9, you'll see the options for building the various installers. You can download various virtual machines as files from InstallAnywhere's web site and have a single installer build multiple versions. You'll note that I'm building a Windows installer, complete with bundled JVM, on a Mac OS X system. Before you click Build, however, click on the Advanced Designer button to switch the wizard to the interactive installer builder.


Figure 9. Build Targets (Simple)

You'll immediately see the Build Targets panel, as shown in Figure 10. As you can see, I'm bundling a JDK-1.4.1-based JVM with my Windows installer. I used a JDK-1.4-specific release of Tomcat (tomcat-4.1.24- LE-jdk14, to be precise), and therefore, I also need to make sure that the Mac OS X installer will require JDK 1.4.


Figure 10. Build Targets (Complex)

In Figure 11, you can see that that's an easy thing to specify.


Figure 11. Requiring the Mac OS X JDK 1.4

Hitting Build will automatically generate installers for Windows, Mac OS X, and Linux, as well as an HTML page and a Java applet that will auto-detect the proper version for the user. This entire page and supporting files, as shown in Figure 12, was generated automatically by InstallAnywhere.


Figure 12. Installation of Web Page

Running the installer for Mac OS X will generate a standard installer, as shown in Figures 13 through 18.


Figure 13. Default Installer Splash


Figure 14. Mac OS X Initial Installer Text


Figure 15. Mac OS XSelect Installation Folder


Figure 16. Mac OS X Platform-Specific Launch Points


Figure 17. Mac OS X Installation Confirmation


Figure 18. Mac OS X Finished Installing

As you can see in Figure 19, the default icon, which is automatically added to the Dock, looks rather nice. Clicking on the icon launches the server user interface as described above, which in turn launches the server and the user's browser (as shown in Figure 20).


Figure 19. Default Mac OS X Icon


Figure 20. Default Web Page

It's worth pointing out that this entire server and application work wonderfully on Windows, as shown in Figures 21-27.


Figure 21. Default Web Page


Figure 22. Default Web Page


Figure 23. Default Web Page


Figure 24. Default Web Page


Figure 25. Default Web Page


Figure 26. Default Web Page


Figure 27. Default Web Page

Summary

A new development and application-packaging model can be a bit intimidating, but it is potentially very powerful. It's easy to imagine using this model to build a digital-hub-style application, one that serves the other systems in your household, regardless of the OS, by accessing the web applications living on other systems in the house, perhaps using Rendevous/ZeroConf to dynamically discover services at runtime. You can add support for document handlers to achieve closer desktop integration, or integrate a database such as hsqldb to provide for a complete database-driven web platform. Or, use this in conjunction with web services for new collaboration and cooperation capabilities. The possibilities abound.

As a final note, I'll be talking more about this topic on Wednesday, October 29th, 2003 at the upcoming O'Reilly Mac OS X Conference. If you can make it, please come by and say "hello."

Will Iverson has been working in the computer and information technology field professionally since 1990.

O'Reilly & Associates recently released (April 2003) Mac OS X for Java Geeks.


Return to ONJava.com.

Copyright © 2009 O'Reilly Media, Inc.