// you’re reading...

Development

JARS, Libraries, Runnables and Ant

For various and sundry reasons I wrote a nice little swing app awhile back which immediately sends me down the rabbit hole of ‘Packaging Java Applications’. Apparently the old adage that Java was designed by PhD’s and implemented by interns holds true in regards to the method of deployment for desktop java apps. Let me share my experience.

Double-clickable JAR

You want users to double click the application, just like any other application. This is actually easy and well-documented, you just create a manifest.xml at the root of the app that looks like this:

Manifest-Version: 1.0
Sealed: true
Main-Class: gui.SyncApp

Replace ‘gui.SyncApp’ with the class that contains your main and your in business. Any recent JRE will launch this for you.

Bundled Libraries

Ok this one is a real bummer. Any reasonably modern Java app will have approx 35,431 JAR files that it requires. Unfortunately JAR files can not reference themselves internally in regards to the Class-Path for some esoteric reason that probably makes great sense on paper but utterly falls down in the Real World.

So when you do the normal ‘Export as JAR’ any bundled up JARs are useless! The answer is to unzip ALL of your jar libraries and bring the packages right into your app. Luckily Eclipse 3.4+ has a new export option “Runnable JAR”. This is a continuation of the very successful Fat-JAR project that has merged into Eclipse and basically decompresses everything for you and also pulls in all none code files for referencing as required.

SideNote: Fat-JAR is not OS X compat but the Export->Runnable seems to work great.

Embedded Databases and ORMS

For this particular app I merged the greatness of Derby (a pure java embedded database) with the fantastic Apache Cayenne ORM. This combination gave me great features in a nice tight package. The problem was how to deploy these things in a JAR file.

Problem 1: Data Source location

Where does the database live since its a file on disk? My app needs to run on Windows, Linux and Mac, all of which have dramatically different file system layouts. The answer here is:

System.getProperty("user.home") + "/myDbName";

This ensures that every OS does the Right Thing.

Problem 2: Tell the ORM where the database is

Now that I have a database how the heck do we tell Cayenne how to find it? Cayenne, like most Java frameworks, requires a static xml file for configuration. Unfortunately for me my database location is dynamic (based on the value of user.home) and the cayenne xml file is does not accept variables (like Ant does). After some great help from the listserv I simply implemented my own DataSourceFactory which turned out to be a lot less scary then I thought it would be.


public DataSource getDataSource(String location) throws Exception {

// Set up the driver, in case it's not on the classpath
Class.forName("org.apache.derby.jdbc.EmbeddedDriver");
String theDatabase = System.getProperty("user.home") + "/myDBName";
String url = "jdbc:derby:" + theDatabase + ";create=true";
String username = "";
String password = "";
String schema = "";

try {
DriverManager.getDriver(url);
} catch (SQLException e) {
log.debug(getClass() + "Failed in registering "
+ "org.apache.derby.jdbc.EmbeddedDriver");
// conn = DriverManager.getConnection(url);
}

// Connect
log.debug("Connecting to " + url);
PoolManager poolManager = new PoolManager(null, // Setting this to null
// will force Cayenne to
// use the DriverManager
url, 1, 1, username, password, new ConnectionLogger());
conn = poolManager.getConnection();

// Check if the schema is up to par
if (!checkSchema()) {
createSchema();
}

conn.close();

// All done!
return poolManager;
}

Now the app can dynamically connect to the proper database for its host OS file system.

First Run…

Next Problem! On first run the database does not yet exist but Cayenne assumes that it DOES exist. This makes Cayenne unhappy, confused and exception prone since connecting works, Derby autocreates the base database, but any real work fails since the database is empty! We need a way to create and populate the database on first run.

Easy enough, we just run some simple checks post-connect and if they fail we build out the tables required.

// Check if the schema is up to par
if (!checkSchema()) {
createSchema();
}

All checkSchema() does is attempt a simple query that should succeed if the table exists (even if it returns 0 rows). If it fails we create the table structures as required using various mechanisms and the createDDL function below. I chose Ant for this, Ant does things like this exceptionally well.

Headless Ants – Creating the table structure on the fly

Just because Ant does something well doesn’t mean that Real Humans can use it. No Real Human is going to execute an Ant script in order to use my cool little tool. This means we have to create Headless Ants that are automated. Easy enough! Ant, like all serious things Java, has an API.


public static void createDDL(String build, String db, String theDatabase) {
Project p = new Project();
p.init();
p.setProperty("dbFile", theDatabase);
File buildFile = new File(build);
File dbFile = new File(db);
System.err.println("File is "+buildFile.length());
ProjectHelper.configureProject(p, buildFile);
p.executeTarget("import-target-db");
}

All we do is pass this function the build.xml path, the db-schema.xml path and the database path. The Ant ddlToDatabase function takes care of the rest for us. Thanks Ant!

Ant Hates JARs

Here is where I really lost it, I simply could NOT coerce Ant into creating the database when running from the JAR file. In Eclipse everything was fine but from the JAR it would constantly bomb out because it could not find the build.xml file. After hours and hours of useless Googling and experimenting I finally discovered the following:

A) You CAN get a handle to the build.xml file using the ClassLoader
java.io.InputStream is = ClassLoader.getSystemResourceAsStream("build.xml");
B) However Ant has no idea how to deal with this, even when you load it as a URL instead of an input stream and pass Ant URL.getFile().

Solution? Copy this file to the users home.dir as .build.xml and pass THAT into the ProjectHelper. Delete when the operation completes.

I also had to do the same with the db-schema.xml file. Once these two files lived outside of the JAR Ant was happy, the database built and the app ran.

Conclusion

None really, once you know how it all snaps together its pretty easy stuff but getting there took a long long time. The docs are terrible and Google can’t find much on the topic that is anything more then other clueless developers begging for help.

Ant does suck in relation to JAR files although I am almost positive I could have done the entire thing in pure Java with no XML somehow. Possibly there is a better solution and way to get Ant to be JAR aware but given how horrible Java support for runnable JARs is I am doubting that this Better Way exists..

Bonus Points

I’ve used Install4J to create native OS packages (both JRE and non-JRE included versions). Fun!

Bummer Points

JNLP (java webstart) runs in a web sandbox so file system access is really difficult. They provide a simple API for file access but since my tools are mostly bundled up jars from other projects it proved exceedingly difficult to teach those how to use the new API for local file access (xml files etc). My solution here was to Give Up since I already had native app installers.

Discussion

Comments are disallowed for this post.

Comments are closed.