Suppose you have developed your Java application and distributed it to your users. If all goes well, the application just works on every computer. But if there's a problem, you have to begin troubleshooting. Users will call for all sorts of installation problems, expecting you to fix them. Moreover, the same problems will often come back: the wrong version of Java, a deleted file, too-restrictive file permissions, etc. Most of these problems can be solved by creating a checklist. However, instead of wasting time asking new users the same questions on the checklist over and over, you can create a diagnostic test that goes through the checklist, providing users with the information they need to solve the problem. If users can't solve the problem themselves, they can show you a clear checklist, so you can take a look at what's going wrong without asking a bunch of questions first.
What problems can users expect? First, things can already go
wrong during the installation process if the user doesn't
follow the installation instructions accurately. Even if the
installation succeeds, problems can appear later. Changes in
configuration (like the JAVA_HOME environment
variable) or changes in the directory structure can indeed break
things. In this article, we will develop an Ant script to run diagnostic tests for
a Java application. We will look at a list of possible problems and
how to deal with them. For our approach to work, Ant has to be
installed on the user's machine. This may mean that your installer
will have to provide Ant.
The first thing you should know to troubleshoot is the system
configuration--the operating system, Java version, classpath,
etc. Implementing this is easy, because Ant provides access to all
Java system properties. Here's an example (reformatted for the
ONJava layout; each <echo> should be on one line):
<?xml version="1.0"?>
<project name="diagnostic" default="all"
basedir=".">
<target name="systemProperties">
<echo message="Java Runtime Environment
version: ${java.version}"/>
<echo message="Java Runtime Environment
vendor: ${java.vendor}"/>
<echo message="Java Runtime Environment
vendor URL: ${java.vendor.url}"/>
<echo message="Java installation
directory: ${java.home}"/>
<echo message="Java Virtual Machine
specification version:
${java.vm.specification.version}"/>
<echo message="Java Virtual Machine
specification vendor:
${java.vm.specification.vendor}"/>
<echo message="Java Virtual Machine
specification name:
${java.vm.specification.name}"/>
<echo message="Java Virtual Machine
implementation version:
${java.vm.version}"/>
<echo message="Java Virtual Machine
implementation vendor:
${java.vm.vendor}"/>
<echo message="Java Virtual Machine
implementation name: ${java.vm.name}"/>
<echo message="Java Runtime Environment
specification version:
${java.specification.version}"/>
<echo message="Java Runtime Environment
specification vendor:
${java.specification.vendor}"/>
<echo message="Java Runtime Environment
specification name:
${java.specification.name}"/>
<echo message="Java class format version
number: ${java.class.version}"/>
<echo message="Java class path:
${java.class.path}"/>
<echo message="List of paths to search when
loading libraries: ${java.library.path}"/>
<echo message="Path of extension directory
or directories: ${java.ext.dirs}"/>
<echo message="Default temp file path:
${java.io.tmpdir}"/>
<echo message="Operating system name:
${os.name}"/>
<echo message="Operating system
architecture: ${os.arch}"/>
<echo message="Operating system version:
${os.version}"/>
</target>
<target name="all" depends="systemProperties"/>
</project>
The example output looks like this (reformatted for this page):
$ ant -f diagnostic.xml
Buildfile: diagnostic.xml
systemProperties:
[echo] Java Runtime Environment version:
1.4.2_05
[echo] Java Runtime Environment vendor:
Apple Computer, Inc.
[echo] Java Runtime Environment vendor URL:
http://apple.com/
...
[echo] Default temp file path: /tmp
[echo] Operating system name: Mac OS X
[echo] Operating system architecture: ppc
[echo] Operating system version: 10.3.9
Now, if something goes wrong, you might be able to see the source of the problem in the system properties: incorrect Java version, class path, etc.
|
Related Reading Ant: The Definitive Guide |
|
We can go further and automate some tests to see if a file or Java class is found:
<target name="files">
<echo message="Testing availability of
needed files..."/>
<available classname="org.apache.fop.apps.Fop"
property="fop.available"/>
<available file="build/scripts" type="dir"
property="scriptsdir.exists"/>
</target>
<target name="fopNotFound" depends="files"
unless="fop.available">
<echo level="error" message="ERROR: Fop
(class org.apache.fop.apps.Fop) not found
in classpath."/>
</target>
<target name="scriptsDirNotFound"
depends="files" unless="scriptsdir.exists">
<echo level="error" message="ERROR:
Directory build/scripts doesn't exist."/>
</target>
The first target performs two tests: first it tests if the class
org.apache.fop.apps.Fop is found in the class path.
Second, it tests if the directory build/scripts exists. By
changing type to file, the
available task will test for the existence of a file with
the specified name. The next two targets show error messages in
case the class isn't available or the directory doesn't exist,
respectively.
We can do even more, but to do so we have to write custom Ant
tasks. Let's check if the installed Java version is greater than a
minimum required version for our Java code. Our diagnostics build
file should show an error if the version isn't OK. The source code
of our JavaVersionTask class is as follows:
import org.apache.tools.ant.*;
/**
JavaVersionTask is an Ant task for testing if
the installed Java version is greater than a
minimum required version.
**/
public class JavaVersionTask extends Task {
// Minimum required Java version.
private String minVersion;
// Installed Java version.
private String installedVersion;
// The name of the property that gets set when
// the installed Java version is ok.
private String propertyName;
/**
* Constructor of the JavaVersionTask class.
**/
public JavaVersionTask() {
super();
installedVersion = System.getProperty
("java.version");
}
/**
* Set the attribute minVersion.
**/
public void setMinVersion(String version) {
minVersion = version;
}
/**
Set the property name that the task sets when
the installed Java version is ok.
**/
public void setProperty(String propName) {
propertyName = propName;
}
/**
* Execute the task.
**/
public void execute() throws BuildException {
if (propertyName==null) {
throw new BuildException("No property name
set.");
} else if (minVersion==null) {
throw new BuildException("No minimum version
set.");
}
if(installedVersion.compareTo(minVersion)
>= 0) {
getProject().setProperty(propertyName,
"true");
}
}
}
|
If you create a custom task, its class has to extend the
org.apache.tools.ant.Task class. Each attribute of the
task in the build file gets set with a set method. The name of the
setter method begins with "set", followed by the name of the
attribute, with the first letter capitalized (this is the JavaBeans
convention). For example, the attribute minVersion
gets set with the method setMinVersion, while the
attribute property gets set with the method
setProperty.
The execute method gets performed when the task
gets called in the build file. First we test if the attributes are
set. Then we do the core of our task: if the installed Java version
is greater than or equal to the minimum required Java version, we
set the value of the property with the specified name to
true.
The versions are stored as String objects. Therefore, we compare
them with the compareTo method, which compares two
Strings alphabetically. This method returns -1 when the first
String comes before the second, 0 when the two Strings are equal,
and +1 when the first String comes after the second alphabetically.
This way, the task treats 1.5 as greater than 1.4, and
also as greater than 1.4.2 or 1.4.2_05.
To use our custom task in the Ant build file, we first define a build property that contains the minimal required Java version:
<property name="minJavaVersion" value="1.5"/>
In the target systemProperties we add the following
code:
<taskdef name="javaversion"
classname="JavaVersionTask" classpath="."/>
<javaversion minVersion="${minJavaVersion}"
property="javaversion.ok"/>
The first line defines a new task with name
javaversion, followed by the name of the Java class
implementing this task, JavaVersionTask. The next line
executes the javaversion task with the minimum
required Java version specified. The property
javaversion.ok gets a value when the installed Java
version is greater than the specified version.
Then we add a target javaVersion:
<target name="javaVersion"
unless="javaversion.ok">
<echo level="error" message="ERROR: Java
version too old: found ${java.version},
needs ${minJavaVersion}."/>
</target>
Next, we add the target to the dependencies of the target
all. It only gets executed if the property
javaversion.ok isn't set. If so, the target shows an
error message stating the found Java version and the required Java
version. An example of the output:
javaVersion: [echo] ERROR: Java version
too old: found 1.4.2_05, needs 1.5.
Analogous to JavaVersionTask, we could also write a
task to perform other version checks, such as for the versions of
installed libraries or the operating system.
When problems occur, it's very important to know whether some
files were changed after the installation. It's possible that the
standard configuration has been changed or a replaced file is
causing the problem. In order to investigate this, we can use Ant's
Checksum: in the installation build file of our
application we generate a checksum for each file, while in our
diagnostic test we verify the checksums. This way we know which
files have been changed after the installation, and the task
narrows down the search for the cause of the problems.
For each important file that could have been changed, we can generate an MD5 checksum in the installation build file and verify it in the diagnostics build file. Of course, there is a difference between changed binary files and changed configuration files. The first shouldn't have changed, and a change in the second could have caused a problem, but not always.
|
In our installation build file, we generate the checksums like this:
<target name="checksum">
<checksum>
<fileset dir="build">
<include name="**/*.class"/>
<include name="config.xml"/>
</fileset>
</checksum>
</target>
For all files with extension .class in the directory
build or its subdirectories, Ant generates an MD5 checksum.
The checksum will be stored in a file named after the original
file's name, with the extension .MD5 added to it. The same happens
with the configuration file config.xml. The target
checksum has to be executed after all files have been
built.
In our diagnostic build file, we verify the checksums:
<target name="checksum">
<echo message="Verifying checksums of binary
files..."/>
<condition property="binary.unchanged">
<checksum>
<fileset dir="build">
<include name="**/*.class"/>
</fileset>
</checksum>
</condition>
<echo message="Verifying checksum of
configuration file..."/>
<condition property="config.unchanged">
<checksum file="build/config.xml"/>
</condition>
</target>
<target name="binaryChanged" depends="checksum"
unless="binary.unchanged">
<echo level="error" message="ERROR: Binary
files changed."/>
</target>
<target name="configChanged" depends="checksum"
unless="config.unchanged">
<echo message="WARNING: Configuration file
changed."/>
</target>
Add the targets to the dependencies of the target
all.
First, the checksum target will be executed. If the
checksums of all class files match the generated checksums, the
property binary.unchanged is set to true.
However, if at least one class file has been changed, the property
doesn't get a value. Next, if the config.xml file is
unchanged, the property config.unchanged is set to
true. The target binaryChanged, which
depends on the target checksum, only gets executed
when binary.unchanged doesn't have a value; that is,
when at least one of the class files has been changed. Then we
output an error message. We do the same with the configuration
file, but we issue the message as a warning.
If no files have been changed, our diagnostics build script outputs this:
checksum:
[echo] Verifying checksums of binary files...
[echo] Verifying checksum of configuration file...
binaryChanged:
configChanged:
all:
BUILD SUCCESSFUL
Total time: 1 second
|
Suppose we change the configuration file. Then we have this output:
checksum:
[echo] Verifying checksums of binary files...
[echo] Verifying checksum of configuration file...
binaryChanged:
configChanged:
[echo] WARNING: Configuration file changed.
all:
BUILD SUCCESSFUL
Total time: 1 second
If our software is open source, we can do even better and propose restoring changed files to their standard versions. For class files specifically, this means we have to recompile the Java files. Of course, your users have to have the whole JDK (not just a Java runtime) and your source code and the build file for your source code. To restore the changed class files, we delete them and call the compilation task of the installation build file. The specifics of this task depend on your build system.
As an alternative (for example, if your software is closed source), you could put known-good versions of the class files in a .jar or .zip in a safe location and unpack them. You can then replace the changed class files with their known-good versions.
On the other hand, configuration files can be copied from the
source directory. In order to do this, we extend the target
configChanged appropriately and add a target
configRestore:
<target name="configChanged"
depends="checksum" unless="config.unchanged">
<echo message="WARNING: Configuration file
changed."/>
<input message="Backup configuration file
and restore original? " validargs="y,n"
addproperty="config.restore"/>
<condition property="config.copy">
<equals arg1="y" arg2="${config.restore}"/>
</condition>
</target>
<target name="configRestore"
depends="configChanged" if="config.copy">
<echo message="Copying build/config.xml to
build/config.xml.1 and restoring configuration
file..."/>
<copy file="build/config.xml"
tofile="build/config.xml.1" overwrite="true"/>
<copy file="src/config.xml" todir="build"
overwrite="true"/>
</target>
Add the targets to the dependencies of the target
all.
If the configuration file has been changed, Ant asks the user if
he wants to back up the configuration file and restore the original.
If he answers "yes" by pressing Y and Enter, the
property config.copy is set. The target
configRestore will only be executed when this property
is set, backing up build/config.xml to
build/config.xml.1 and copying the original
src/config.xml to build/config.xml.
We developed an Ant script to run diagnostic tests for a Java application. The script checks whether the version of the Java installation meets a minimum requirement, if some important files haven't been changed, if a specific Java class is in the class path, if a directory exists, etc. After checking all of these prerequisites of the software, the script reports the results to the user. The script can even repair some problems. In addition, the output of the diagnostic test can be used by technical support to help the user quickly, without asking him a whole list of questions.
Koen Vervloesem has a master's degree in computer science and has been freelancing as an IT journalist since 2000, primarily for Dutch IT magazines.
Return to ONJava.com.
Copyright © 2009 O'Reilly Media, Inc.