Wednesday, December 30, 2009

The BugRunner Application









The BugRunner Application


The BugRunner code is unchanged from Chapter 11, aside from the addition of one new method in the BugRunner class, which is explained in the next section.



Preparing the JARs


I'm assuming that the target machine for the installation doesn't have Java 3D installed, so the test machine where I develop the installation shouldn't have it either. Instead, j3dutils.jar and J3DUtils.dll are placed in the BugRunner/ directory (as shown in Figure A-4).



Figure A-4. The BugRunner/ directory




Since Java 3D isn't installed in a standard location checked by javac and java, the calls to the compiler and JVM must include additional classpath information. The compileBR.bat batch file in BugRunner/ contains this line:



javac -classpath "%CLASSPATH%;j3dutils.jar" *.java



The BugRunner.bat batch file contains a similar line:



java -cp "%CLASSPATH%;j3dutils.jar" BugRunner



There's no need to mention J3DUtils.dll, which will be found by the JAR as long as it's in the same directory.


Once the program has been tested, the classes and all other application resources must be packaged up as JARs prior to being passed to install4j. The BugRunner application consists of various classes and the two subdirectories, Images/ and Sounds/. These should be thrown together into a single BugRunner.jar file, along with any DLLs. The makeJar.bat batch file contains the following line:



jar cvmf mainClass.txt BugRunner.jar *.class *.dll Images Sounds



The manifest details in mainClass.txt are as follows:



Main-Class: BugRunner
Class-Path: j3dutils.jar



The manifest specifies the class location of the application's main( ) method and adds j3dutils.jar to the classpath used when BugRunner.jar is executed. For this example, the application assumes this JAR in the same directory as BugRunner.jar.


Since you're controlling the installation process, this isn't that big of a requirement and not a limitation.



The DLL is stored in the JAR because the installation is easier if install4j only has to deal with JARs. However, after the installation EXE file has been downloaded to the user's machine, the DLL must be removed from BugRunner.jar and written to the new BugRunner/ directory. This copying from the JAR to the local directory is carried out by the BugRunner class, which is the new method I mentioned earlier. main( ) in BugRunner calls an installDLL( ) method:



public static void main(String args[])
{
// DLLs used by Java 3D J3DTimer extension
installDLL("J3DUtils.dll");

long period = (long) 1000.0/DEFAULT_FPS;
new BugRunner(period*1000000L); // ms --> nanosecs
}


private static void installDLL
(String dllFnm)
/* Installation of the DLL to the local directory
from the JAR file containing BugRunner. */
{
File f = new File(dllFnm);
if (f.exists( ))
System.out.println(dllFnm + " already installed");
else {
System.out.println("Installing " + dllFnm);
// access the DLL inside this JAR
InputStream in = ClassLoader.getSystemResourceAsStream
(dllFnm);
if (in == null) {
System.out.println(dllFnm + " not found");
System.exit(1);
}
try { // write the DLL to a file
FileOutputStream out = new FileOutputStream(dllFnm);

// allocate a buffer for reading entry data.
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1)
out.write(buffer, 0, bytesRead);

in.close( );
out.flush( );
out.close( );
}
catch (IOException e)
{ System.out.println("Problems installing " + dllFnm); }
}
} // end of installDLL( )



installDLL( ) will be called every time that BugRunner is executed, so installDLL( ) first checks whether the DLL is present in the local directory. If not, it's installed by being written out to a file. The stream from the DLL file inside the JAR is created with:



InputStream in = ClassLoader.getSystemResourceAsStream(dllFnm);



This works since the DLL is in the same JAR as BugRunner, so the class loader for BugRunner can find it. Figure A-5 illustrates the technique.



Figure A-5. Installing a DLL locally




The result is that the BugRunner application installed on the user's machine will consist of two JARs, BugRunner.jar and j3dutils.jar. After the first execution of BugRunner, these two JARs will be joined by J3DUtils.dll.




Testing


The best testing approach is to move the two JARs to a different machine, specifically one that doesn't have Java 3D installed (but J2SE or JRE must be present). Double-click on BugRunner.jar, and the game should begin. The J3DUtils.dll will magically appear in the same directory. Alternatively, the application can be tested with this command:



java -jar BugRunner.jar



Executing this in a command window will allow standard output and error messages to be seen on the screen, which can be useful for testing and debugging.


You could stop at this point since the game is nicely wrapped up inside two JARs. However, an installer will allow a professional veneer to be added, including installation screens, splash windows, desktop and menu icons, and an uninstaller. These are essential elements for most users.




Creating the BugRunner Installer


This is not a book about install4j, so I'll only consider the more important points in creating the BugRunner installer. install4j has an extensive help facility and links to a tutorial at its web site at http://www.ej-technologies.com/products/install4j/tutorials.html. Another good way of understanding things is to browse through the various screens of the BugRunner installation script, bugRun.install4j. The screenshots are taken from install4j v.2.0.7; the latest release has modified some of the configuration screens and added lots of new features.


A crucial step is to define the distribution tree, the placement of files and directories in the BugRunner/ directory created on the user's machine at install time. My approach is to include an Executables/ subdirectory to hold the two JAR files. This directory structure is shown in Figure A-6.


A quick examination of Figure A-6 will reveal three JARs inside Executables/: BugRunner.jar, j3dutils.jar, and custom.jar. custom.jar contains Java code deployed by the uninstaller, which is explained below.


BugRunner.jar is not called directly but via an EXE file, which is set up on the Configure executable screen during the Launchers stage (shown in Figure A-7).


The executable's name is BugRunner.exe and will be placed in the Executables/ directory along with the JARs. It's important to set the working directory to be ".", so the JAR will search for resources in the correct place.


Under the Advanced Options button, redirecting stdout and stderr into log files is possible and a good idea for testing and debugging.




Figure A-6. Distribution tree for the BugRunner installer





Figure A-7. Configuring the BugRunner executable




The EXE file must be told which JAR file holds the main( ) method for the application and which JARs are involved in the application's execution. This is done through the Configure Java Invocation screen shown in Figure A-8.



Figure A-8. Configuring the Java invocation




Figure A-8 shows that custom.jar (holding the uninstaller code) isn't part of the application since it isn't included in the Class path list.


The GUI Installer stage (shown in Figure A-9) allows the customization of various stages of the installation: the welcome screen, tasks to be done before installation, tasks after installation, and the finishing phase. The screen on the right of Figure A-9 is for the installer actions, which contains uninstallation actions.


Perhaps the best advantage of using install4j is its close links to Java, most evident in the way the installation (and uninstallation) process can be customized. install4j offers a Java API for implementing many tasks, and the install4j distribution comes with an example installer that uses the API's capabilities.


For example, the default action carried out prior to uninstallation is to call the DLLUninstallAction class in custom.jar, so application-specific tasks can be defined by overriding that class. I use this technique in the next section.




Uninstallation


The default install4j uninstaller will delete the JARs that it added to the Executables/ directory along with all the other files and directories it created at installation time. However, the installer didn't add J3DUtils.dll to Executables/; that task was carried out by the BugRunner class when it first ran.



Figure A-9. The installer/uninstaller actions for the GUI installer




install4j knows nothing about J3DUtils.dll, so it won't delete it or the subdirectory that holds it. The outcome is that a basic BugRunner uninstaller won't remove the BugRunner/Executables/ directory or the J3DUtils.dll file inside it. To get around this, you need to define a pre-uninstall operation that removes the DLL, so the main uninstaller can delete everything else.


custom.jar contains the DLLUninstallAction class, which extends install4j's UninstallAction class. UninstallAction offers two abstract methods for custom behavior:



public abstract boolean performAction(Context context, ProgressInterface pi);
public abstract int getPercentOfTotalInstallation( );



The uninstallation task (in this case, deleting the DLL) should be placed inside performAction( ). User messages can be sent to the uninstallation screen via the ProgressInterface object. performAction( ) should return true if the task is successful or false to abort the uninstallation process. getPercentOfTotalInstallation( ) gets the amount of the progress slider assigned to the task, returning a number between 0 and 100.


My implementation of performAction( ) obtains a list of the DLLs in the Executables/ directory and then deletes each one. This approach is more flexible than hardwiring the deletion of J3DUtils.dll into the code and means that the same DLLUninstallAction class can be employed with the Checkers3D installer discussed later:



private static final String PATH = "../Executables";
// location of the DLLs relative to <PROG_DIR>/.install4j


public boolean performAction(Context context, ProgressInterface progReport)
// called by install4j to do uninstallation tasks
{
File delDir = new File(PATH);

FilenameFilter dllFilter = new FilenameFilter( ) {
public boolean accept(File dir, String name)
{ return name.endsWith("dll"); }
};

String[] fNms = delDir.list(dllFilter); // list of dll filenames
if (fNms.length == 0)
System.out.println("Uninstallation: No DLLs found");
else
deleteDLLs(fNms, progReport);
return true;
// end of performAction( )
}



The tricky aspect of the code is the use of the PATH variable. The uninstaller is executed in the .install4j/ directory, which is at the same level as Executables/. Both of these are located in the BugRunner/ directory installed on the user's machine (see Figure A-10).



Figure A-10. The installed BugRunner directories




The PATH string redirects the File object to refer to Executables/. A list of filenames ending in ".dll" is collected by using a FileFilter anonymous class, and the list is passed to deleteDLLs( )
:



private void deleteDLLs(String[] fNms,ProgressInterface progReport)
// delete each DLL file, and report the progress
{
progReport.setStatusMessage("Deleting installed DLLs");

int numFiles = fNms.length;
String msg;
for (int i=0; i < numFiles; i++) {
msg = new String("" + (i+1) + "/" + numFiles + ": " + fNms[i] + "... ");
deleteFile(fNms[i], progReport, msg);
progReport.setPercentCompleted( ((i+1)*100)/numFiles );
try {
Thread.sleep(500); // 0.5 sec to see something
}
catch (InterruptedException e) {}
}
}



deleteDLLs( ) loops through the filenames, calling deleteFile( ) for each. The ProgressInterface object informs the user of the progress of these deletions.


deleteFile( ) creates a File object for the named file and then calls File.delete( ).



The DLLUninstallAction class must be compiled with the install4j API classes added to the classpath:



javac -classpath "%CLASSPATH%;d:\install4j\resource\i4jruntime.jar"
DLLUninstallAction.java



The creation of the JAR file is standard:



jar cvf custom.jar *.class





The BugRunner Installer


The resulting installer, called BR_1.0.exe, takes a few seconds to generate and is about 1.4 MB.


The size will drop to 950 KB if the timer-specific version of j3dutils.jar is employed.



A version bundled with JRE 1.4.2 comes in at 12 MB.










    No comments: