Skip to main content

Make Your Swing App Go Native, Part 2

January 5, 2004

{cs.r.title}






Welcome back to this series on making Swing applications behave more
like native applications. In the previous article, we created
a simple chat program with OS-specific menus and alerts. It sort
of feels native, but we still have to start it from the command line. It
would be nice if there were a double-clickable icon on the desktop to
start our program.

In this installment, we will create native
executables for Mac OS X and Windows, then add another native feature:
file type associations. In the third installment we will add custom
icons and finishing polish.

Automation

As we add icons and file associations, we need the process to be
repeatable and automatable. Using GUI tools and wizards is fine for
while you are learning, but in production, the last thing you want to do
is open Photoshop and scale icons for every build. Everything we do
here will be run from Ant, the
popular Java build tool, so all work needs to be done by custom Ant
tasks or scriptable utilities.

If you haven't used Ant before, you should really check it out. Ant is similar
to the old Unix make, in that it uses a project description file
to build portions, or the entirety, of programs. Ant is different in that it uses XML
files instead of plain text, and all of its functions are implemented as
cross-platform Java classes instead of shell scripts that may not be
present on your computer. Though it's only been around for a few years,
it has quickly proven to be the tool of choice for building Java
applications.

Packaging Options

The ideal Java application package is one that behaves just like a
native app. For desktop systems, this means that the user can double click on
an icon to launch the program. The icon is custom for the program,
and the windows and taskbar/dock all have matching icons. This is the
ideal scenario. Now, how to get there? To package up an application. we
have a couple of options:

  • Double-clickable .jar files
  • Shell scripts
  • .apps and .exes

First we could just make a double-clickable .jar. This is where the
user receives a .jar file and can double click on it to start the
program. The .jar has extra information in the manifest to tell the JVM
where the startup class is and what other .jars need to be included in
the classpath. In this case, double clicking on the .jar would be
equivalent to doing a java -jar myprogram.jar command. This
is a great way to keep it simple, but it doesn't feel quite right.
There's no custom icon, as Figure 1 illustrates, and the application doesn't take advantage of
native features. In fact, its icon and description make it look like a document,
not an application. Still, it does have the ability to launch the program
without a command line. It's pretty simple and shouldn't be overlooked
when you are sending out betas.

Figure 1
Figure 1. Double clickable .jar on Mac OS X

Our second option is using a batch file or Mac .command script to launch
the program. This may be somewhat better, since we can probably get a custom icon and
the user no longer has to know which of the many .jars you sent is the
one he or she needs to double click, but it still doesn't take advantage of native hooks.

It looks like we need something better: real .apps and
.exes.

Building a Double-Clickable OS X Application

On the Mac, we can create a real
application bundle,
which gives the program access to the same features as a native application.
An application bundle (commonly called a .app) is similar to a .exe under Windows in
that it is a double-clickable application, but instead of being a simple
binary, it is actually a folder with
a .app extension. The extension is
usually hidden, but a listing from the command line will show it. One of
the really cool things about .apps is that they
contain an entire directory structure with standard places
for resources (icons and text), and XML config files for setting
every conceivable option. In particular, we will set the startup
classfile and display options. So as not to rehash what's been written
elsewhere, including Apple's documentation, I won't explain all of the available features. Instead, we'll focus on automating it with an Ant script.

To build an application bundle, we first need to create the directory structure.
Here's part of a dist-mac target from an Ant build.xml
file to do that:

<target name="dist-mac" depends="jar">
    <property name="appdir"
        value="${dist-mac}/${app-name}.app"/>
    <mkdir dir="${appdir}"/>
    <mkdir dir="${appdir}/Contents"/>
    <mkdir dir="${appdir}/Contents/MacOS"/>
    <mkdir dir="${appdir}/Contents/Resources"/>
    <mkdir dir="${appdir}/Contents/Resources/Java"/>

This assumes that the variables ${dist-mac} and
${app-name} have been previously defined. Such an
arrangement lets us decide later where to put the files and makes the
task more reusable.

Next, we need to get a copy of the application stub, which is the binary
code that actually starts your Java program. This stub is helpfully
provided by Apple in the developer toolkit, in /System/Library/Frameworks/JavaVM.framework/
Versions/Current/Resources/MacOS/
. Since we need to use the 1.3 JVM to support our JDirect calls
to bounce the dock icon (see the previous article), I have copied the older one to src/packaging so it won't conflict with the normal dev tools.

<copy file="${packaging}/JavaApplicationStub"
  todir="${appdir}/Contents/MacOS"/>
<exec command="chmod 755
${appdir}/Contents/MacOS/JavaApplicationStub"/>

A quick note here: I've added the chmod command here because Ant will
not preserve the executable bit. Unfortunately, Java has no concept of
Unix file permissions, only the ability to read and write. So we
use Ant's exec task to call out the shell command
chmod. Hopefully a future version of Java (or a standard extension) will address this.

Next we need to copy in the Info.plist file.

<copy file="${packaging}/Info.plist"
    todir="${dist-mac}/${app-name}.app/Contents"/>

The Info.plist file is an XML configuration file that
completely defines an application. It tells OS X the name of the
application, how to start it, which icons to use, which files it
understands, and virtually every other config parameter you can imagine.
To create it, we will start with a sample file provided by Apple and make
a few changes. We will set the main class to our Startup
class and include the joshy-common.jar support lib. Below
is a portion of the file:

<key>CFBundleIconFile</key>
<string>GenericJavaApp.icns</string>
<key>Java</key>
<dict>
    <key>MainClass</key>
    <string>org.joshy.oreilly.swingnative.Startup</string>
    <key>JVMVersion</key>
    <string>1.3+</string>
    <key>ClassPath</key>
      <array>
      <string>$JAVAROOT/joshy-common.jar</string>
      </array>
    <key>Properties</key>
    <dict>
      <key>com.apple.mrj.application.apple.menu.about.name</key>
      <string>MadChatter</string>
      <key>com.apple.macos.useScreenMenuBar</key>
      <string>true</string>
    </dict>

Notice that in the <dict> element, we've also
included the command line parameters discussed in the previous article
to set the application name and use the main Mac menu bar.

Now, back to our Ant file. Next we copy the application itself, which
consists of our code .jars and any support library .jars.

<copy file="${build}/${app-name}.jar"
  todir="${appdir}/Contents/Resources/Java"/>
<copy todir="${appdir}/Contents/Resources/Java">
    <fileset dir="lib">
        <include name="*.jar"/>
    </fileset>
</copy>

The last thing we need to do is tell OS X that this directory isn't
just a directory, but a real live application. We do this with another
exec call to a command-line tool, setfile.

<exec command="/Developer/Tools/SetFile -a B ${appdir}"/>

Run the osx.app target and we get a double-clickable application,
as seen in Figure 2:

Figure 2
Figure 2. Mac OS X Java application bundle







Creating a Double-Clickable .exe for Windows

On the Windows side, we have to use a completely different method
because neither Microsoft nor Sun provides any tools for packaging Java
apps. There are, however, several third-party tools to do these tasks.
For this article, I will use a commercial tool,
JexePack by DuckWare, that I've had great luck with in the past. It costs $100 for commercial use,
but you can freely download a fully functional trial version.

First we will call jexepack from our Ant file, which is
a simple exec call:

<target name="exe" depends="assemble">
    <exec dir="." executable="${bin}/jexepack.exe">
        <arg value="${packaging}/jexepack.ini"/>
    </exec>
</target>

We've stored the command-line options in an .ini file so that we can
modify them easily and keep our Ant file from being cluttered. It also
makes version control easier, since it's a separate text file.

The jexepack.ini file looks like this:

# turn on verbose output
/v
# make it a windowed application
/w
# set the class with the main() method in it
/main:org.joshy.oreilly.swingnative.Startup
# the output exe filename
/out:dist\MadChatter.exe

# include the class files, images, support .exes, and html files
/dir:classes
/r
*.class
/dir:lib
/r
*.jar
/jar:joshy-common.jar

This text file will create an executable in the dist directory, setting it to be a
windowed application (hiding the stdout console window),
sets the main class file, and includes all of the relative classes. Note
that we have to specify the .jars twice, once to make sure they are
included in the final .exe (the /r *.jar part),
and then once to make sure they are in the classpath and available to
the application (the /jar:joshy-common.jar part).
Unfortunately, the names of the .jars and class files have to be hardcoded in this text file. Hopefully DuckWare will make Ant tasks in a
future version, or we could use custom tasks to edit the file in place.
We won't need to change it very often, so this is okay.

Run the exe task to produce the double-clickable .exe
shown in figure 3.

Figure 3
Figure 3. Windows native Java executable

Creating File Type Associations for OS X

Now that we have icons out of the way, let's work on something a
little more daring. Our chat application might want to save log files.
When a user clicks on the log file, we would like our application to
launch and open the file. This is typical behavior for native
applications, so we should do it too. Not an easy task, but we've got some
help.

Let's say our log file ends with .mchat (which means
"MadChatter log file") — we want to tell the OS to find our application and launch it when any .mchat file is clicked. That means our application needs to receive an event saying
it should open the file named filename.mchat.

On the Mac, we can use Apple's ApplicationListener
interface to pick up the open event. The XP library introduced in the previous article handles most of this for us, but the code below shows what we need to do. It subclasses the ApplicationAdapter,
which provides no-op implementations of all of the
methods in ApplicationListener, and
overrides the handleOpenFile() method to do our work.

Application app = new Application();
getApplication().addApplicationListener(new ApplicationAdapter() {
    public void handleOpenFile(ApplicationEvent evt) {
      System.out.println("opening");
      ChatPanel panel = Startup.newWindow();
      String file = evt.getFilename();
      // u is a utility class included in the XP lib
      String text = u.file_to_string(file);
      panel.textarea.setText(text);
    }
});

On the packaging side, we need to tell OS X about our app's desire to
get certain files. Back in our Info.plist file, we add a few lines
to do this:

<key>CFBundleDocumentTypes</key>
<array>
    <dict>
        <key>CFBundleTypeExtensions</key>
        <array>
            <string>mchat</string>
        </array>
        <key>CFBundleTypeName</key>
        <string>MadChatter Log File</string>
        <key>CFBundleTypeRole</key>
        <string>Viewer</string>
    </dict>
</array>

All file-type configuration is stored inside of the
CFBundleDocumentTypes key. It's an array because we could
have more than one document type associated with our application. Each
document definition has a list of extensions, the name of the type of
file, and the role. The name is the description that will be shown in
the Finder. The role specifies what our application can do with it, which
usually means whether we can edit or just view the files. This controls
the listings in open/save dialog boxes and other desktop behavior that
doesn't matter as much to us. The CFBundleDocumentTypes key
can also be used to assign creator codes, set icons, and other nifty
things. For more information, see Apple's Cocoa documentation on document types.

Figure 4 shows that the Mac Finder understands the association of
.mchat files to the Mad Chatter application. Double-clicking
the file will now tell our application to load it.

Figure 4
Figure 4. OS X filetype associations







Filetypes For Windows

Windows doesn't have a clean text-file way of setting filetype
associations. Instead, we will have to muck with the registry, which
isn't the easiest thing to do. There are no standard APIs to help, but
there are some options. We will start with an open source JNI library
called
JNIRegistry,
created by Tim Endres and ICE Engineering. Unfortunately, it's not
terribly well documented, and again, Windows registry hacking isn't easy
for programmers used to the very open and structured Java world. Being a
Java programmer, I had no idea where to start. After a few minutes of
Googling, however, I did find some code in the jtorrent CVS tree on Source Forge that uses JNIRegistry to do exactly the same thing we want to do:
associate our program with a file extension.

The Windows registry is essentially a tree of nested keys. To view,
edit, or delete keys you have to find them and then manipulate them. The
keys we want all start in the top level key of HKCR. This
is the HKEY_CLASSES_ROOT portion of the registry database, which contains
"file extensions associations, CLSIDs file and object types." (More information can be found here.)

String topLevelKey = "HKCR";
RegistryKey topKey =
    Registry.getTopLevelKey(topLevelKey);

Now we want to create a new key to hold our new values:

RegistryKey localKey =
    topKey.createSubKey(key, value,
                        RegistryKey.ACCESS_WRITE);

The value itself has to be held in another structure, the
RegStringValue:

RegStringValue val =
    new RegStringValue(localKey, valueName, value);

Now we can set the value and save the key:

localKey.setValue(val);
localKey.flushKey();

I've wrapped all of this up in a static function called setKey, which
takes a key, value name, and value. The key is the actual name of the key we want to set. The value
name specifies which value in this key we want to set. In our
case, we use "" for the default value name. The actual value is
the string we are setting. (It is also possible to put numbers
and binary data into the registry, but that is beyond the scope
of this article.) A full list of the available Windows keys can be found
here.

Now that we have the ability to set a key, what keys do we want to set? First we need to tell Windows
that the file extension .mchat is associated with our application:

setKey(".mchat","","MadChatter");

Then we need to set the description of MadChatter files:

setKey("MadChatter","","MadChatter logfile");

Now Windows recognizes .mchat files, but it doesn't know what
to do with them. Let's tell it we can open them and what to open them
with:

String command = exeDir() + "\\MadChatter.exe "%1"";
setKey("MadChatter\\shell","","open")
setKey("MadChatter\\shell\\open\command","",command)

The first setKey() says we know how to open files and the second
gives the command-line program that will do it. The command is three
parts: the location of our .exe, the .exe
itself, and the place where the path of the requested file will be
stored. The first part, exeDir(), is a utility function
that retrieves the jexepack.exe property. JexePack, our
Windows packaging program, stores the directory of our .exe here. If
jexepack.exe is missing, then exeDir() will
return the current working directory instead.

I've wrapped all of this code into a single static utility function,
RegTest.setAppString(), which takes the filename extension, the
command string, the name of the program, and the description of the
filetype. It will be called in the main function at program startup, which
ensures that the keys are always set to the right values.

public static final String EXTENSION = "mchat";
[...]
public void main(String[] args) {
[...]
// if on windows then run the registry code
if(!xp.isMac()) {
    String command = exeDir() + "\\MadChatter.exe "%1"";
    RegTest.setAppString("."+Startup.EXTENSION,command,
      "madchatter","MadChatter logfile");
}

That's all we need to tell Windows about our app without using an
installer. This strategy has a limitation, however. If the user ever
moves our program, the filetype association will be broken until our
program is run again. This is really a limitation of the Windows
filesystem, because unlike the Mac's HFS, there is no path-independent
way of finding a file on the hard drive. Usually, however, people just
put the application in one place and never move it again, so we should
be pretty safe.

To run it, we have to load up the JNIRegistry libraries. We can add
the .jar file (registry.jar) to our build.xml
easily enough, but dealing with the .dll is trickier. It
turns out that native libraries are loaded via the system path, which
we'd rather not mess with. However, the system path already includes the
current directory, so if we just put the .dlls there we are
all set. So we copy the .dll from the lib dir to the
working dir in our build file.

<target name="run" depends="compile">
    <copy file="lib/ICE_JNIRegistry.dll" todir="."/>
    <java fork="true"
      classpath=
       "${classes};lib/joshy-common.jar;lib/registry.jar"
      classname=
       "org.joshy.oreilly.swingnative.Startup"/>
</target>

Now this is fine for testing from the command line, but what about
our generated .exe? Well, JexePack has already thought of
that. As long as we put the .dll and extra .jars in the build
set and add the /jni option, JexePack will take care of the
rest. So we add few more lines to our jexepack.ini file:

/jar:registry.jar
/jni
ICE_JNIRegistry.dll

Run ant win-dist and we are ready to go. File Explorer
recognizes our application type and will load it when the user double
clicks on .mchat files, as illustrated by Figure 5:

Figure 5
Figure 5. Windows filetype associations

Conclusion

Thank you for joining me for the second part of my series on making
Swing applications feel native. Last time, we worked on the menus and
added alerts. This time, we bundled our program into real executables and
added filetype associations. Mad Chatter is starting to feel like a
real application now. I hope you will join me for the final article,
where we will create custom icons, a splash screen, and a few more bits
of polish to make The Mad Chatter really shine.

Editor's Note: Downloadable sample code for this article and others
in the series will be provided at the conclusion of the series. You can also participate in the Mad Chatter project, part of java.net's Java Desktop community.

Josh Marinacci first tried Java in 1995 at the request of his favorite TA and has never looked back.
Related Topics >> GUI   |   Swing   |   Web Design   |