|
Create Desktop Applications with Java-Based Web Technologiesby Will Iverson, Mac OS X Conference speaker and author of Mac OS X for Java Geeks09/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.
Let's look at some examples of desktop applications where the primary interface is through a web browser:
|
Author conference session 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 |
CUPS, the printing architecture of Mac OS X 10.2 (Jaguar)--from which comes the original printing user interface--is available right now for anyone using Jaguar. If you're running Jaguar, try it out.
Radio Userland, a web publishing tool, uses HTML as its primary user interface.
BEA WebLogic, JBoss, and other web application servers use a browser UI for administration and configuration. Apple recently announced that it would be bundling JBoss in a future version of Mac OS X.
WebAdmin, a web-based console, is one of the better tools for managing a system. (Check out the screenshots at www.webmin.com/screens.html.)
ZOË, a web application that runs in the background, grabbing and indexing all of your incoming and outgoing email, is one of the more innovative contenders. You do most of your work with your email client as normal, but you can also open up ZOË's web interface and plumb all of your email's associations.
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.
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.
We'll be using the following materials to build this application and the installer:
The Tomcat binaries from the Apache Jakarta web site.
A bit of code from the SourceForge.net-hosted BrowserLauncher project, which will allow our application to easily launch the user's default web browser.
An example of embedded the Tomcat web server from another article on the OnJava web site by James Goodwill.
The ZeroG InstallAnywhere Now installer creator, the free version of the company's installation product.
A copy of JDK 1.4 or later. You can get the latest version from Apple via Software Update, or from Sun Microsystems for Windows and other platforms.
|
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. |
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.
|
Related Reading Mac OS X for Java Geeks |
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.
|
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.
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
|
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).
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
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.
Chapter 10: "QuickTime for Java," is available free online.
You can also look at the Table of Contents, the Index, and the full description of the book.
For more information, or to order the book, click here.
Return to ONJava.com.
Copyright © 2009 O'Reilly Media, Inc.