ONJava.com    
 Published on ONJava.com (http://www.onjava.com/)
 See this if you're having trouble printing code examples


Diagnostic Tests with Ant

by Koen Vervloesem
10/12/2005

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.

System Configuration

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.

Ant: The Definitive Guide

Related Reading

Ant: The Definitive Guide
By Steve Holzner

Availability of Files and Classpaths

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.

Minimum Required Java Version

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.

Changed Files

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.

Conclusion

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.

Resources

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.