10.6 Starting Tomcat with Ant

10.6.1 Problem

You want to start Tomcat using Ant.

10.6.2 Solution

Create a target that invokes the custom Ant task com.oreilly.javaxp.tomcat.tasks.StartTomcatTask.

10.6.3 Discussion

Typically, a server is started from a command prompt using a predefined script distributed with the server. To facilitate test-first programming, we need the ability to start Tomcat from Ant. Specifically, the Ant build process needs to start Tomcat and wait for Tomcat to become available before continuing. As of this writing, there is no generic way to solve this problem. So we created a new Ant task called StartTomcatTask to provide the functionality needed.

Example 10-4 shows the AbstractTomcatTask, which is the base class for the StartTomcatTask (Example 10-5) and StopTomcatTask (Recipe 10.7). This task extends Ant's Task, and is directly referenced in a buildfile.

Example 10-4. AbstractTomcatTask
package com.oreilly.javaxp.tomcat.tasks;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;

public abstract class AbstractTomcatTask extends Task {

    private TomcatSupport tomcatSupport;

    /**
     * Overrides the base class implementation to instantiate the
     * <code>TomcatSupport</code> class.
     */
    public void init(  ) {
        this.tomcatSupport =
                new TomcatSupport(this, getScriptToExecute(), isStarting(  ));
    }

    /**
     * @return the name of the script to execute. For Tomcat 4.0 and
     * higher a valid filename might be 'startup'. The name of the script
     * should <strong>not</strong> include the extension.
     */
    public abstract String getScriptToExecute(  );

    /**
     * @return true if Tomcat is being started; false if Tomcat is being
     * stopped.
     */
    public abstract boolean isStarting(  );

    /**
     * Called by Ant to start the execution of the target.
     */
    public void execute(  ) throws BuildException {
        this.tomcatSupport.execute(  );
    }

    /**
     * Called by Ant to set the attribute 'testURL'. This attribute is
     * referenced in the buildfile.
     *
     * @param testURL a URL that is used to connect to Tomcat. This URL
     * is used to validate that Tomcat is running.
     */
    public void setTestURL(String testURL) {
        this.tomcatSupport.setTestURL(testURL);
    }

    /**
     * Called by Ant to set the attribute 'catalinaHome'. This attribute is
     * referenced in the buildfile.
     *
     * @param catalinaHome the full path to where Tomcat is installed.
     */
    public void setCatalinaHome(String catalinaHome) {
        this.tomcatSupport.setCatalinaHome(catalinaHome);
    }

    /**
     * @param timeout a number representing the timeout in
     * milliseconds. The timeout must be greater than 10 seconds
     * (10000 ms) and less than 60 seconds (60000 ms).
     */
    public void setTimeout(String timeout) {
        try {
            long temp = Long.parseLong(timeout);
            if (temp >= 10000 && temp <= 60000) {
                this.tomcatSupport.setTimeout(temp);
            } else {
                throw new BuildException("Invalid 'timeout' value: "
                        + timeout + ". The timeout must be between " +
                        "10000 and 60000.");
            }
        } catch (NumberFormatException nfe) {
            throw new BuildException("Invalid 'timeout' value: " + timeout);
        }
    }
}

This task defines three attributes:

  1. The testURL attribute specifies a URL that is used to determine when the server is started. For example, http://localhost:8080.

  2. The catalinaHome attribute specifies where Tomcat is installed. This is the same as the environment variable CATALINA_HOME.

  3. The timeout attribute specifies how long the task should wait before failing. Typically, Tomcat starts in 10-15 seconds, depending on the computer. Anything over 60 seconds is too long.

Example 10-5 shows the StartTomcatTask. This task extends from Abstract-TomcatTask and provides the implementation for the getScriptToExecute( ) and isStarting( ) methods.

Example 10-5. StartTomcatTask
package com.oreilly.javaxp.tomcat.tasks;

public class StartTomcatTask extends AbstractTomcatTask {

    /**
     * @return the script name "startup" without any extension.
     */
    public String getScriptToExecute(  ) {
        return "startup";
    }

    public boolean isStarting(  ) {
        return true;
    }
}

Example 10-6 shows the support class TomcatSupport used by the new task.

Example 10-6. TomcatSupport class
package com.oreilly.javaxp.tomcat.tasks;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;

public class TomcatSupport {

    private Task task;
    private URL testURL;
    private String catalinaHome;
    private String scriptToExecute;
    private long timeout;
    private boolean isStarting;

    /**
     * @param task the task that invoked this class. It's used only
     * for logging.
     * @param scriptToExecute the Tomcat 4.1.12 script used to start
     * or stop the server.
     */
    public TomcatSupport(Task task,
                         String scriptToExecute,
                         boolean isStarting) {
        this.task = task;
        this.scriptToExecute = scriptToExecute;
        this.isStarting = isStarting;

        // I have a pretty slow machine and this seems to be long enough.
        this.timeout = 20000;
    }

    /**
     * Executes a Tomcat script to start or stop the server.
     */
    public void execute(  ) throws BuildException {

        // if the server is up and we are trying to start it then return
        // if the server is down and we are tryint to stop it then return
        if (isTomcatRunning(  ) == this.isStarting) {
            if (this.isStarting) {
                this.task.log("Tomcat is already started!");
            } else {
                this.task.log("Tomcat is *not* running!");
            }
            return;
        }

        runScript(  );

        boolean didTimeout = true;
        this.timeout = System.currentTimeMillis(  ) + this.timeout;
        while (System.currentTimeMillis(  ) < this.timeout) {
            sleep(500);
            if (isTomcatRunning(  ) == this.isStarting) {
                didTimeout = false;
                break;
            }
        }

        if (didTimeout) {
            throw new BuildException("The server was not started " +
                    "successfully. Please make sure that your buildfile " +
                    "specifies a long enough timeout period AND that " +
                    "your environment is setup correctly.");
        }

        if (this.isStarting) {
            this.task.log("Tomcat is started!");
        } else {
            this.task.log("Tomcat is stopped!");
        }
    }

    public void runScript(  ) throws BuildException {
        validateScript(this.catalinaHome, this.scriptToExecute);

        this.task.log((this.isStarting ? "Starting" : "Stopping") +
                " Tomcat...");

        try {
            Runtime.getRuntime(  ).exec(this.catalinaHome +
                    File.separator + "bin" + File.separator +
                    this.scriptToExecute);
        } catch (IOException e) {
            throw new BuildException(e);
        }
    }

    public void setTestURL(String testURL) {
        try {
            this.testURL = new URL(testURL);
        } catch (MalformedURLException e) {
            throw new BuildException("Invalid URL: " + testURL);
        }
    }

    public void setCatalinaHome(String catalinaHome) {
        this.catalinaHome = catalinaHome;
    }

    public void setScriptToExecute(String scriptToExecute) {
        this.scriptToExecute = scriptToExecute;
    }

    public void setTimeout(long timeout) {
        this.timeout = timeout;
    }

    private boolean isTomcatRunning(  ) {

        HttpURLConnection conn = null;
        try {
            conn = (HttpURLConnection) this.testURL.openConnection(  );
            conn.connect(  );
            isURLValid(conn);
            return true;
        } catch (IOException e) {
            logException(e);
            return false;
        } finally {
            if (conn != null) {
                conn.disconnect(  );
            }
        }
    }

    private boolean isURLValid(HttpURLConnection conn) {
        int responseCode = 0;
        try {
            responseCode = conn.getResponseCode(  );
            // Response Codes 400 and above represent errors according
            // to the HTTP 1.1 specification available in RFC 2616 at
            // http://www.ietf.org/rfc/rfc2616.txt
            return (responseCode >= 100 && responseCode < 400);
        } catch (IOException e) {
            logException(e);
            return false;
        }
    }

    private void logException(Exception e) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream(  );
        PrintWriter writer = new PrintWriter(baos);
        e.printStackTrace(writer);
        writer.close(  );
        this.task.log(new String(baos.toByteArray(  )), Project.MSG_DEBUG);
    }

    private void sleep(long ms) {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException e) {
        }
    }

    private static void validateScript(String path, String script) {
        File file = new File(path + File.separator + "bin" +
                File.separator + script);
        if (!file.exists() || file.isDirectory(  )) {
            throw new BuildException("Invalid File: " + 
                    file.getAbsolutePath(  ));
        }
    }
}

The execute method drives this support class, and is called from the StartTomcatTask.execute( ) method. Here are the main steps this method executes:

  1. Invoke the method isTomcatRunning( ) to determine if Tomcat is already started. If so, then exit the method.

  2. The runScript( ) method is executed to start Tomcat. Tomcat is started in a new JVM courtesy of Tomcat's startup script.

  3. Keep invoking the isTomcatRunning( ) method until the timeout is exceeded or Tomcat is started.

  4. Once the server is started, the execute( ) method relinquishes control to Ant and the other tasks are allowed to execute if the timeout has not been exceeded. If the task timed out, the build process stops.

Example 10-7 shows how to use this task in an Ant buildfile.

Example 10-7. Starting Tomcat through Ant
<target name="start.tomcat">
  <taskdef name="starttomcat"   
      classname="com.oreilly.javaxp.tomcat.tasks.StartTomcatTask">
    <classpath>
      <path location="${dir.build}"/>
    </classpath>
  </taskdef>

  <starttomcat
      testURL="${host}:${port}"
      catalinaHome="${env.CATALINA_HOME}"
      timeout="30000"/>
</target>

10.6.4 See Also

Recipe 10.7 shows how to use the custom task StopTomcatTask to stop Tomcat from an Ant buildfile. This task patiently waits until the server is completely stopped before relinquishing control to other Ant targets.