In my previous post I had said that the ReverseEngineer functionality for Log4J had proven to be most useful in a servlet environment. Here is the servlet that makes it so. With this ConfigServlet, a web application can with a single page display the active Log4J configuration in a simple text box and allow the user to change that configuration and submit it back to the web application to change the Log4J configuration on the fly.
What this means is no more needing to set log levels to debug or trace at the beginning of the container resulting in huge amounts of log entries you don't need to see. Don't change the log settings until just before you test! Need to peek into a running system? Or you decide to examine just one category of logs? Or perhaps to suppress others? No need to shut down the web application and restart, just a simple load, alter and submit.
Of course, there is a significant security risk here, this page should not be visible to untrusted users. For my own usage, I was developing and maintaining a web application so this one servlet saved me a significant amount of time and effort by enabling me to be both quicker and more precise.
ConfigServlet is very primitive as servlets go, just spitting out a simple HTML form with two radio buttons, a text area, and a submit button. It could certainly be significantly beautified for today's frameworks. For this, though, style was irrelevant and functionality rules, so here it is in base form.
To set the configuration, the servlet takes query or post parameters for format (Property or XML) and config (the text content). Whether or not the configuration is set, both the property-style output and the DOM-style output for the current Log4J configuration are generated and available by selecting the desired radio button on the form. The same radio button determines which format is used to reconfigure Log4J if the submit button is selected. The radio button is the format parameter and the textarea contents are the config parameter back to the same servlet.
Below the ConfigServlet is a HTTPUnit 1.6-based JUnit 3.8 test that directly tests the ConfigServlet. Always write your tests!
package log4j.servlet;
import java.io.*;
import java.net.URL;
import java.net.MalformedURLException;
import java.util.*;
import java.util.regex.*;
import javax.servlet.ServletException;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.http.*;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.apache.log4j.PropertyConfigurator;
import org.apache.log4j.xml.DOMConfigurator;
import org.xml.sax.SAXException;
import log4j.util.ReverseEngineer;
import org.apache.log4j.Logger;
import org.apache.log4j.LogManager;
import org.apache.log4j.helpers.OptionConverter;
/**
Provided as a HTML fragment.
*/
public class ConfigServlet extends HttpServlet
{
protected void doPost (HttpServletRequest request, HttpServletResponse response) throws IOException
{
doGet(request, response);
}
protected void doGet (HttpServletRequest request, HttpServletResponse response) throws IOException
{
process(request, response);
}
public static void process (HttpServletRequest request, HttpServletResponse response) throws IOException
{
configure(request.getParameter("format"), request.getParameter("config"), response.getWriter());
generate(request.getRequestURI(), response.getWriter());
}
private static void configure (String format, String config, PrintWriter out) throws IOException
{
if (format != null && config != null)
{
if (format.equals("Property"))
{
Properties propertyConfig = new Properties();
propertyConfig.load(new ByteArrayInputStream(config.getBytes()));
PropertyConfigurator.configure(propertyConfig);
out.print("<p class=\"log4jConfig\">Logging configured!</p>");
}
else if (format.equals("XML"))
{
try
{
new DOMConfigurator().doConfigure(new ByteArrayInputStream(config.getBytes()), LogManager.getLoggerRepository());
out.print("<p class=\"log4jConfig\">Logging configured!</p>");
}
catch (FactoryConfigurationError ex)
{
out.print("<p class=\"log4jConfig.error\">XML parsing failed: ");
out.print(ex.getMessage());
out.print("</p>");
}
}
else
{
out.print("<p class=\"log4jConfig.error\">Unknown format: ");
out.print(format);
out.print("</p>");
}
}
}
private static void generate (String contextPath, PrintWriter out) throws IOException
{
StringWriter sw = new StringWriter();
ReverseEngineer.writeDOMConfiguration(LogManager.getLoggerRepository(), sw);
String dom = sw.toString();
ByteArrayOutputStream bout = new ByteArrayOutputStream();
ReverseEngineer.writePropertyConfiguration(LogManager.getLoggerRepository(), bout);
String properties = new String(bout.toByteArray());
String newLine = System.getProperty("line.separator");
out.print("<form ID=\"log4jConfig\" name=\"log4jConfig\" method=\"get\" action=\"");
out.print(contextPath);
out.print("\">");
out.print(newLine);
out.print(newLine);
out.print("<p>");
out.print(newLine);
out.print("<span class=\"log4jConfig\">Format:</span>");
out.print(newLine);
out.print("<input ID=\"log4jConfig.propertyFormat\" type=\"radio\" name=\"format\" value=\"Property\" checked onclick=\"document.log4jConfig.config.value = '");
out.print(Pattern.compile("\r?\n").matcher(ReverseEngineer.escapeXml(properties)).replaceAll("\\\\n"));
out.print("';\"/> <span class=\"log4jConfig\">Properties</span> ");
out.print(newLine);
out.print("<input ID=\"log4jConfig.xmlFormat\" type=\"radio\" name=\"format\" value=\"XML\" onclick=\"document.log4jConfig.config.value = '");
out.print(Pattern.compile("\r?\n").matcher(ReverseEngineer.escapeXml(dom)).replaceAll("\\\\n"));
out.print("'\"/> <span class=\"log4jConfig\">XML</span> ");
out.print(newLine);
out.print("</p>");
out.print(newLine);
out.print(newLine);
out.print("<p>");
out.print(newLine);
out.print("<span class=\"log4jConfig\">Configuration</span>");
out.print(newLine);
out.print("</p>");
out.print(newLine);
out.print(newLine);
out.print("<textarea ID=\"log4jConfig.config\" name=\"config\" cols=\"100\" rows=\"25\" wrap=\"off\">");
out.print(properties);
out.print("</textarea>");
out.print("<p>");
out.print(newLine);
out.print("<input ID=\"log4jConfig.submit\" type=\"submit\" value=\"Configure\"/>");
out.print(newLine);
out.print("<input ID=\"log4jConfig.reset\" type=\"reset\" value=\"Start Over\"/>");
out.print(newLine);
out.print("</p>");
out.print(newLine);
out.print(newLine);
out.print("</form>");
out.print(newLine);
out.flush();
}
public void init (ServletConfig config) throws ServletException
{
ServletContext context = config.getServletContext();
String log4jConfig = config.getInitParameter("log4j.configuration");
String log4jConfiguratorClass = config.getInitParameter("log4j.configuratorClass");
if (log4jConfig != null)
{
try
{
URL log4jConfigUrl = context.getResource(log4jConfig);
if (log4jConfigUrl != null)
{
OptionConverter.selectAndConfigure(log4jConfigUrl, log4jConfiguratorClass, LogManager.getLoggerRepository());
Logger.getLogger(getClass()).info("Log4J configured from " + log4jConfig + " using " + log4jConfiguratorClass);
}
}
catch (MalformedURLException badUrl)
{
Logger.getLogger(getClass()).warn("Could not load Log4J configuration from " + log4jConfig + " using " + log4jConfiguratorClass, badUrl);
}
}
}
}
package unit.logging;
import java.io.*;
import java.util.*;
import java.util.regex.Pattern;
import javax.xml.parsers.DocumentBuilderFactory;
import org.apache.log4j.*;
import org.apache.log4j.spi.*;
import org.apache.log4j.xml.*;
import junit.framework.*;
import com.meterware.httpunit.*;
import com.meterware.servletunit.*;
import log4j.util.*;
public class ConfigServletTest extends TestCase
{
public static final Logger LOGGER = Logger.getLogger(ConfigServletTest.class);
public ConfigServletTest (String name)
{
super(name);
}
public void testPropertyConfig () throws Exception
{
ServletRunner servletRunner = new ServletRunner();
servletRunner.registerServlet("/config", "log4j.servlet.ConfigServlet");
ServletUnitClient client = servletRunner.newClient();
WebRequest request = new PostMethodWebRequest("http://localhost/config");
WebResponse response = client.getResponse(request);
String config = response.getFormWithID("log4jConfig").getParameterValue("config");
Properties propertiesConfig = new Properties();
propertiesConfig.load(new ByteArrayInputStream(config.getBytes()));
LoggerRepository repository = LogManager.getLoggerRepository();
Properties propertiesConfig2 = ReverseEngineer.getPropertyConfiguration(repository);
assertEquals("Property configuration did not match", propertiesConfig2, propertiesConfig);
repository.resetConfiguration();
request = new PostMethodWebRequest("http://localhost/config");
response = client.getResponse(request);
Properties resetConfig = new Properties();
resetConfig.load(new ByteArrayInputStream(response.getFormWithID("log4jConfig").getParameterValue("config").getBytes()));
assertFalse("Property configuration did not reset", propertiesConfig2.equals(resetConfig));
assertEquals("Configuration failed to match after reset", ReverseEngineer.getPropertyConfiguration(repository), resetConfig);
WebForm configForm = response.getFormWithID("log4jConfig");
configForm.setParameter("config", config);
response = configForm.submit();
Properties propertiesConfig3 = new Properties();
propertiesConfig3.load(new ByteArrayInputStream(response.getFormWithID("log4jConfig").getParameterValue("config").getBytes()));
assertEquals("Restored property configuration did not match", propertiesConfig2, propertiesConfig3);
}
public void testDOMConfig () throws Exception
{
LoggerRepository repository = LogManager.getLoggerRepository();
ServletRunner servletRunner = new ServletRunner();
servletRunner.registerServlet("/config", "log4j.servlet.ConfigServlet");
ServletUnitClient client = servletRunner.newClient();
WebRequest request = new PostMethodWebRequest("http://localhost/config");
WebResponse response = client.getResponse(request);
WebForm configForm = response.getFormWithID("log4jConfig");
configForm.setParameter("format", "XML");
String config = Pattern.compile("\r?\n").matcher(configForm.getParameterValue("config")).replaceAll(System.getProperty("line.separator"));
StringWriter sw = new StringWriter();
ReverseEngineer.writeDOMConfiguration(repository, sw);
String config2 = Pattern.compile("\r?\n").matcher(sw.toString()).replaceAll(System.getProperty("line.separator"));
assertEquals("XML configurations did not match", config2, config);
repository.resetConfiguration();
request = new PostMethodWebRequest("http://localhost/config");
response = client.getResponse(request);
configForm = response.getFormWithID("log4jConfig");
configForm.setParameter("format", "XML");
String resetConfig = configForm.getParameterValue("config");
assertFalse("XML configurations did not reset", resetConfig.equals(config));
configForm.setParameter("config", config);
response = configForm.submit();
configForm = response.getFormWithID("log4jConfig");
configForm.setParameter("format", "XML");
String config3 = Pattern.compile("\r?\n").matcher(configForm.getParameterValue("config")).replaceAll(System.getProperty("line.separator"));
assertEquals("Restored XML configuration did not match", config2, config3);
}
}
Tuesday, July 17, 2012
Monday, July 16, 2012
ReverseEngineer for Log4J
I love Log4J. Long before the Java logging API, Log4J version 1.2 had better features and gave me everything I wanted. Well, almost everything. There were a few things missing that I desired to have. Thankfully, Log4J is open source and I was free to add to it. Perhaps version Log4J 2.0 will be able to satisfy me, but until then I can work with my own modifications.
One shortcoming was that I could not look at the current configuration of Log4J for a running process. I could look at the configuration files -- if I could figure out which configuration file was actually being used -- and if the configuration had not been changed in memory.
My solution was to write a class that would examine the Log4J configuration in memory and derive the appropriate configuration text or object for either PropertyConfigurator or DOMConfigurator that could be used to recreate the current Log4J configuration. This code would have to be executed in the running JVM, so the running program would have to provide access in some fashion. I have found this to be most useful as a Java Servlet, but more on that later.
Here is the source code for ReverseEngineer. Below is also a JUnit 3.8 test case that tests it.
One shortcoming was that I could not look at the current configuration of Log4J for a running process. I could look at the configuration files -- if I could figure out which configuration file was actually being used -- and if the configuration had not been changed in memory.
My solution was to write a class that would examine the Log4J configuration in memory and derive the appropriate configuration text or object for either PropertyConfigurator or DOMConfigurator that could be used to recreate the current Log4J configuration. This code would have to be executed in the running JVM, so the running program would have to provide access in some fashion. I have found this to be most useful as a Java Servlet, but more on that later.
Here is the source code for ReverseEngineer. Below is also a JUnit 3.8 test case that tests it.
Thursday, July 12, 2012
AntForkTask for Ant
Ant already has <ant> and <antcall>, but these two tasks have one particular restriction: They both execute in the same VM as the Ant program executing them. What if I want to run Ant in a separate VM? Ok, you can accomplish this using the <java> task with fork=true, but that takes a lot more effort to set up each time. It is far from resuable, and in fact is rather frustrating.
I actually had a lot of reasons to run Ant in a separate VM, so I wrote a task to do this. It looks like this:
I have found this most useful for adding JUnit
to the classpath in order to use the JUnit and JUnitReport tasks. AntFork is also useful in combination
with the CallOver task in order to distribute load. One virtual
machine may not be up to the task of several parallel targets munching
up memory. Parallel targets that use AntFork can allow the separate virtual machines to deal with memory-intensive processes.
In fact, I found the AntForkTask so useful that I was astonished that this functionality was not added to Ant before, and I am completely dumbfounded that over 7 years later that Ant still lacks this capability.
Here is the source code that I used for Ant 1.5. I have no doubt it could use some updating for the latest version of Ant.
/*
Copyright 2005 Steven S. Morgan
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
steven@technicalabilities.com
http://www.technicalabilities.com/java
*/
package com.technicalabilities.ant;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Iterator;
import java.util.Vector;
import java.util.Properties;
import java.util.Map;
import java.util.Set;
import java.util.HashSet;
import java.util.Enumeration;
import org.apache.tools.ant.BuildListener;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.taskdefs.Java;
import org.apache.tools.ant.taskdefs.Property;
import org.apache.tools.ant.types.Commandline;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.Reference;
import org.apache.tools.ant.types.Environment;
/**
<p>
The AntForkTask allows an ant task to be executed in a new process.
Memory hogs such as the <tt><style></tt> task can be executed
separately and the memory recovered. Computer processing power can
be used more effectively. Alternative ant versions and JVMs can also
be used.
</p>
<p>
Most attributes and elements supported by the <tt><java></tt> and
<tt><ant></tt> tasks are supported here.
</p>
@author Steven Morgan
@version 1.0
*/
public class AntForkTask extends Task
{
/**
Properties that should not be passed along!
*/
private static final Set FILTER_PROPERTIES = new HashSet();
static
{
FILTER_PROPERTIES.add("line.separator");
FILTER_PROPERTIES.add("file.separator");
}
/**
The Project for the forked ant task. A separate Project is required.
If the task were forked from the current Project, then the Process
would not be garbage collected after completion due to hanging references.
*/
private Project javaProject;
/**
Whether or not environment variables will be inherited.
The default value is <tt>true</tt>
*/
private boolean inheritAll = true;
/**
The name of the ant build file to be processed.
There is no default value; this must be specified.
*/
private String buildFile;
/**
The name of the target in the ant build file to be processed.
There is no default value; the ant build file's default will
be processed if no target is specified.
*/
private String target;
/**
The location of the ant installation to be used for the forked task.
The default is the currently executing installation.
*/
private File antHome = new File(System.getProperty("ant.home"));
/**
The Java installation to be used for the forked task. The default
is the value in the platform's environment variable JAVA_HOME.
*/
private File javaHome;
/**
Should the emacs flag be indicated for the forked ant task.
The default value is <tt>false</tt>.
*/
private boolean emacs = false;
/**
The classname of the logger to be used by the forked ant task.
There is no default value. If not set, no logger is specified
to the forked ant task.
*/
private String logger;
/**
The classname of the event listener to be used by the forked ant task.
There is no default value. If not set, no event listener is specified
to the forked ant task.
*/
private String listener;
/**
The filename of the log file to be used by the forked ant task.
There is no default value. If not set, no log file is specified
to the forked ant task.
*/
private File logfile;
/**
The classname of the input handler to be used by the forked ant task.
There is no default value. If not set, no input handler is specified
to the forked ant task.
*/
private String inputHandler;
/**
The properties specified in this task's definition for this specific
execution.
*/
private Vector propertyList = new Vector();
/**
The properties inherited from the current environment by the forked
ant task.
*/
private Properties properties = new Properties();
/**
Should the quiet flag be indicated for the forked ant task.
The default value is <tt>false</tt>.
*/
private boolean quiet = false;
/**
Should the verbose flag be indicated for the forked ant task.
The default value is <tt>false</tt>.
*/
private boolean verbose = false;
/**
Should the debug flag be indicated for the forked ant task.
The default value is <tt>false</tt>.
*/
private boolean debug = false;
/**
The Java task generated from the new Project that will be executing
ant in a separate process.
@see #javaProject
*/
private Java java;
/**
Create a new AntForkTask.
*/
public AntForkTask () {}
/**
Initialize this AntForkTask. The separate Project and the Java task
are created and fixed values are set here.
@throws BuildException if thrown during the creation or initialization
of the new Project or the Java task
*/
public void init () throws BuildException
{
javaProject = new Project();
javaProject.setJavaVersionProperty();
javaProject.addTaskDefinition("java",(Class) getProject().getTaskDefinitions().get("java"));
java = (Java)javaProject.createTask("java");
java.setOwningTarget(getOwningTarget());
java.setTaskName("antfork");
java.setFork(true);
java.setClassname("org.apache.tools.ant.Main");
java.setDir(getProject().getBaseDir());
}
private void addProperties (Map propertyMap)
{
for (Iterator propIter = propertyMap.entrySet().iterator();
propIter.hasNext();
)
{
Map.Entry nextProp = (Map.Entry)propIter.next();
java.createArg().setValue("-D" + nextProp.getKey() + "=" + nextProp.getValue());
log("Added property " + nextProp.getKey() + " -> " + nextProp.getValue(), Project.MSG_VERBOSE);
}
}
/**
Execute ant in a separate process according to the current configuration.
@throws BuildException if the forked ant process could not be executed
or if thrown by the Java task
*/
public void execute() throws BuildException
{
javaProject.setInputHandler(getProject().getInputHandler());
for (Iterator listeners = getProject().getBuildListeners().iterator();
listeners.hasNext();
)
{
javaProject.addBuildListener((BuildListener)listeners.next());
}
if (antHome == null)
{
throw new BuildException("antHome must be set");
}
if (!antHome.exists())
{
throw new BuildException("antHome does not exist: " + antHome);
}
Environment.Variable antHomeSysVar = new Environment.Variable();
antHomeSysVar.setKey("ant.home");
antHomeSysVar.setFile(antHome);
java.addSysproperty(antHomeSysVar);
FileSet antLibs = new FileSet();
antLibs.setDir(new File(antHome, "lib"));
antLibs.createInclude().setName("**/*.jar");
Path classpath = createClasspath();
classpath.addFileset(antLibs);
for (Enumeration propEnum = propertyList.elements();
propEnum.hasMoreElements();
)
{
((Property)propEnum.nextElement()).execute();
}
// Put all properties in one Properties object
// Inherited properties first
Properties definedProperties = new Properties();
if (inheritAll)
{
definedProperties.putAll(getProject().getProperties());
log("Inheriting all: " + definedProperties, Project.MSG_DEBUG);
}
else
{
definedProperties.putAll(getProject().getUserProperties());
log("Inheriting user: " + definedProperties, Project.MSG_DEBUG);
}
// Remove predefined properties, as best as possible, so as not to confuse ant
for (Iterator propertyIter = definedProperties.keySet().iterator();
propertyIter.hasNext();
)
{
String name = (String)propertyIter.next();
if (name.startsWith("ant.") || name.startsWith("java.") ||
name.startsWith("os.") || name.startsWith("user.") ||
name.startsWith("sun.") ||
FILTER_PROPERTIES.contains(name))
{
propertyIter.remove();
}
}
// Specified properties second, thus overriding any
// inherited properties
definedProperties.putAll(properties);
log("Explicitly defined: " + properties, Project.MSG_DEBUG);
// Remove basedir or ant will be unable to set it
definedProperties.remove("basedir");
log("All defined properties for antfork: " + definedProperties, Project.MSG_DEBUG);
addProperties(definedProperties);
if (javaHome == null)
{
// Messy to get JAVA_HOME, but this is how other ant tasks do it
Project tempProject = new Project();
Property envProperties = new Property();
envProperties.setProject(tempProject);
envProperties.setEnvironment("env");
envProperties.execute();
javaHome = new File((String)tempProject.getProperty("env.JAVA_HOME"));
tempProject = null;
envProperties = null;
}
if (!javaHome.exists())
{
throw new BuildException("javaHome does not exist: " + javaHome);
}
// Reproducing classpath logic from the ant scripts.
// Add tools.jar and classes.zip as needed.
File javaLibs = new File(javaHome, "lib");
if (javaLibs.exists())
{
File toolsJar = new File(javaLibs, "tools.jar");
if (toolsJar.exists())
{
classpath.createPathElement().setLocation(toolsJar);
}
toolsJar = null;
File classesZip = new File(javaLibs, "classes.zip");
if (classesZip.exists())
{
classpath.createPathElement().setLocation(classesZip);
}
classesZip = null;
}
javaLibs = null;
if (emacs)
{
java.createArg().setValue("-emacs");
}
if (quiet)
{
java.createArg().setValue("-quiet");
}
if (verbose)
{
java.createArg().setValue("-verbose");
}
if (debug)
{
java.createArg().setValue("-debug");
}
if (logfile != null)
{
java.createArg().setValue("-logfile");
try
{
java.createArg().setValue(logfile.getCanonicalPath());
}
catch (IOException ioe)
{
throw new BuildException("Could not determine canonical path for " + logfile, ioe);
}
}
if (logger != null)
{
java.createArg().setValue("-logger");
java.createArg().setValue(logger);
}
if (listener != null)
{
java.createArg().setValue("-listener");
java.createArg().setValue(listener);
}
if (inputHandler != null)
{
java.createArg().setValue("-inputHandler");
java.createArg().setValue(inputHandler);
}
if (buildFile != null)
{
java.createArg().setValue("-buildfile");
java.createArg().setValue(buildFile);
}
if (target != null)
{
java.createArg().setValue(target);
}
// The REAL work
java.execute();
// Null everything to allow garbage collection
javaProject = null;
java = null;
antHome = null;
propertyList = null;
properties = null;
javaHome = null;
logfile = null;
// Request garbage collection so that the underlying Procses
// object doesn't hang around longer than necessary.
System.gc();
}
/**
If true, pass all properties to the new Ant project.
Defaults to <tt>true</tt>.
@param value <tt>true</tt> to inherit the current execution's
properties, <tt>false</tt> to not inherit
*/
public void setInheritAll(boolean value)
{
inheritAll = value;
}
/**
The build file to use.
@param f the ant build file to be processed by the forked ant task
*/
public void setBuildfile(String f)
{
this.buildFile = f;
}
/**
The target of the new Ant project to execute.
Defaults to the new project's default target.
@param s the target in the ant build file to be processed by the
forked ant task
*/
public void setTarget(String s)
{
this.target = s;
}
/**
Set the emacs flag for the forked ant task.
@param b <tt>true</tt> if task labels should be dropped,
<tt>false</tt> to display them
*/
public void setEmacs (boolean b)
{
this.emacs = b;
}
/**
Set the quiet flag for the forked ant task.
@param b <tt>true</tt> if the forked ant task should be quiet,
<tt>false</tt> if it should report messages normally
*/
public void setQuiet (boolean b)
{
this.quiet = b;
}
/**
Set the verbose flag for the forked ant task.
@param b <tt>true</tt> if the forked ant task should be verbose,
<tt>false</tt> if it should report messages normally
*/
public void setVerbose (boolean b)
{
this.verbose = b;
}
/**
Set the debug flag for the forked ant task.
@param b <tt>true</tt> if the forked ant task should be debugged,
<tt>false</tt> if it should report messages normally
*/
public void setDebug (boolean b)
{
this.debug = b;
}
/**
Property to pass to the new project.
The property is passed as a 'user property'
@return a new AntForkProperty
@see AntForkProperty
*/
public Property createProperty()
{
Property property = new AntForkProperty();
property.setTaskName("property");
property.setProject(javaProject);
propertyList.add(property);
return property;
}
/**
Set the ant installation to be run in the new process.
@param f the desired ant installation's location
*/
public void setAntHome (File f)
{
this.antHome = f;
}
/**
Set the Java installation to be run in the new process.
@param f the desired Java installation's location
*/
public void setJavaHome (File f)
{
this.javaHome = f;
}
/**
Set the log file to be used by the forked ant process.
@param f where ant should log its results
*/
public void setLogfile (File f)
{
this.logfile = f;
}
/**
Set the logger to be used by the forked ant process.
@param s the name of the desired logger class
*/
public void setLogger (String s)
{
this.logger = s;
}
/**
Set the event listener to be used by the forked ant process.
@param s the name of the desired event listener class
*/
public void setListener (String s)
{
this.listener = s;
}
/**
Set the input handler to be used by the forked ant process.
@param s the name of the desired input handler class
*/
public void setInputHandler (String s)
{
this.inputHandler = s;
}
/**
Set the classpath to be used for the forked Java execution. This
is the classpath under which ant will be executed.
@param s the Path to be used for the JVM executing ant in the
new process
*/
public void setClasspath(Path s)
{
java.createClasspath().append(s);
}
/**
Creates a nested classpath element to be added to the classpath
for the new process.
@return a new Path that is part of the classpath the Java process
will use for running ant
*/
public Path createClasspath()
{
return java.createClasspath();
}
/**
Adds a reference to a CLASSPATH defined elsewhere.
@param r the classpath to be used by the Java process when running ant
*/
public void setClasspathRef(Reference r)
{
javaProject.addReference(r.getRefId(), r.getReferencedObject(getProject()));
java.createClasspath().setRefid(r);
}
/**
Set the command line arguments for the JVM that will be running ant.
@param s the arguments to be used, delimited by spaces
*/
public void setJvmargs(String s)
{
java.setJvmargs(s);
}
/**
Creates a nested jvmarg element describing a single command line
argument for the JVM that will be running ant.
@return a new command line argument for the JVM
*/
public Commandline.Argument createJvmarg()
{
return java.createJvmarg();
}
/**
Set the command used to start the JVM.
@param s the command to start the JVM
*/
public void setJvm(String s)
{
java.setJvm(s);
}
/**
Add a nested sysproperty element. This value will be visible to ant
in the forked process.
@param sysp the desired system property for the JVM
*/
public void addSysproperty(Environment.Variable sysp)
{
java.addSysproperty(sysp);
}
/**
Throw a BuildException if process returns non 0.
@param fail <tt>true</tt> if an error reported by the forked ant process
should abort the current ant process, <tt>false</tt> if the
current process should continue on
*/
public void setFailonerror(boolean fail)
{
java.setFailonerror(fail);
}
/**
The working directory of the forked ant process
@param d the directory in which the JVM process should be executed
*/
public void setDir(File d)
{
java.setDir(d);
}
/**
File the output of the process is redirected to.
@param out the file where the JVM's output will be captured
*/
public void setOutput(File out)
{
java.setOutput(out);
}
/**
Set the maximum memory that the JVM may use.
@param max the maximum memory to formatted as expected by the JVM
*/
public void setMaxmemory(String max)
{
java.setMaxmemory(max);
}
/**
Specify the Java version to which the JVM complies. This is
currently used by the Java command line support in ant to determine
the maximum memory paramter (-mx for 1.1, -Xmx otherwise).
@param value the Java version supported by the JVM (i.e., "1.3.1")
*/
public void setJVMVersion(String value)
{
java.setJVMVersion(value);
}
/**
Use a completely new environment.
@param newenv <tt>true</tt> if the execution environment should be
duplicated for the forked process, <tt>false</tt> if not
*/
public void setNewenvironment(boolean newenv)
{
java.setNewenvironment(newenv);
}
/**
Add a nested env element - an environment variable.
@param var the environment variable to be set for the forked process
*/
public void addEnv(Environment.Variable var)
{
java.addEnv(var);
}
/**
This subclass of Property is used internally by AntForkTask to capture
all the properties being set for the forked ant task.
*/
private class AntForkProperty extends Property
{
/**
By overriding addProperty, all property name/value pairs are
captured so that they can be saved to a separate file.
@param name the property name
@param value the property value
*/
protected void addProperty (String name, String value)
{
properties.put(name, value);
}
}
}
I actually had a lot of reasons to run Ant in a separate VM, so I wrote a task to do this. It looks like this:
<target name="doTest"> <antfork buildfile="test/build-test.xml" target="test" verbose="${verbose}" emacs="yes"> <classpath> <fileset dir="test/lib" includes="**"/> </classpath> <property name="test.type" value="${test.type}"/> <property name="log4j.config" location="${log4j.config}"/> <property name="module.build" value="${module.build}"/> <property name="module.dir" value="${module.dir}"/> <property name="module.compile.classpath" value="${module.compile.classpath}"/> </antfork> </target>
I have found this most useful for adding JUnit

In fact, I found the AntForkTask so useful that I was astonished that this functionality was not added to Ant before, and I am completely dumbfounded that over 7 years later that Ant still lacks this capability.
Here is the source code that I used for Ant 1.5. I have no doubt it could use some updating for the latest version of Ant.
/*
Copyright 2005 Steven S. Morgan
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
steven@technicalabilities.com
http://www.technicalabilities.com/java
*/
package com.technicalabilities.ant;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Iterator;
import java.util.Vector;
import java.util.Properties;
import java.util.Map;
import java.util.Set;
import java.util.HashSet;
import java.util.Enumeration;
import org.apache.tools.ant.BuildListener;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.taskdefs.Java;
import org.apache.tools.ant.taskdefs.Property;
import org.apache.tools.ant.types.Commandline;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.Reference;
import org.apache.tools.ant.types.Environment;
/**
<p>
The AntForkTask allows an ant task to be executed in a new process.
Memory hogs such as the <tt><style></tt> task can be executed
separately and the memory recovered. Computer processing power can
be used more effectively. Alternative ant versions and JVMs can also
be used.
</p>
<p>
Most attributes and elements supported by the <tt><java></tt> and
<tt><ant></tt> tasks are supported here.
</p>
@author Steven Morgan
@version 1.0
*/
public class AntForkTask extends Task
{
/**
Properties that should not be passed along!
*/
private static final Set FILTER_PROPERTIES = new HashSet();
static
{
FILTER_PROPERTIES.add("line.separator");
FILTER_PROPERTIES.add("file.separator");
}
/**
The Project for the forked ant task. A separate Project is required.
If the task were forked from the current Project, then the Process
would not be garbage collected after completion due to hanging references.
*/
private Project javaProject;
/**
Whether or not environment variables will be inherited.
The default value is <tt>true</tt>
*/
private boolean inheritAll = true;
/**
The name of the ant build file to be processed.
There is no default value; this must be specified.
*/
private String buildFile;
/**
The name of the target in the ant build file to be processed.
There is no default value; the ant build file's default will
be processed if no target is specified.
*/
private String target;
/**
The location of the ant installation to be used for the forked task.
The default is the currently executing installation.
*/
private File antHome = new File(System.getProperty("ant.home"));
/**
The Java installation to be used for the forked task. The default
is the value in the platform's environment variable JAVA_HOME.
*/
private File javaHome;
/**
Should the emacs flag be indicated for the forked ant task.
The default value is <tt>false</tt>.
*/
private boolean emacs = false;
/**
The classname of the logger to be used by the forked ant task.
There is no default value. If not set, no logger is specified
to the forked ant task.
*/
private String logger;
/**
The classname of the event listener to be used by the forked ant task.
There is no default value. If not set, no event listener is specified
to the forked ant task.
*/
private String listener;
/**
The filename of the log file to be used by the forked ant task.
There is no default value. If not set, no log file is specified
to the forked ant task.
*/
private File logfile;
/**
The classname of the input handler to be used by the forked ant task.
There is no default value. If not set, no input handler is specified
to the forked ant task.
*/
private String inputHandler;
/**
The properties specified in this task's definition for this specific
execution.
*/
private Vector propertyList = new Vector();
/**
The properties inherited from the current environment by the forked
ant task.
*/
private Properties properties = new Properties();
/**
Should the quiet flag be indicated for the forked ant task.
The default value is <tt>false</tt>.
*/
private boolean quiet = false;
/**
Should the verbose flag be indicated for the forked ant task.
The default value is <tt>false</tt>.
*/
private boolean verbose = false;
/**
Should the debug flag be indicated for the forked ant task.
The default value is <tt>false</tt>.
*/
private boolean debug = false;
/**
The Java task generated from the new Project that will be executing
ant in a separate process.
@see #javaProject
*/
private Java java;
/**
Create a new AntForkTask.
*/
public AntForkTask () {}
/**
Initialize this AntForkTask. The separate Project and the Java task
are created and fixed values are set here.
@throws BuildException if thrown during the creation or initialization
of the new Project or the Java task
*/
public void init () throws BuildException
{
javaProject = new Project();
javaProject.setJavaVersionProperty();
javaProject.addTaskDefinition("java",(Class) getProject().getTaskDefinitions().get("java"));
java = (Java)javaProject.createTask("java");
java.setOwningTarget(getOwningTarget());
java.setTaskName("antfork");
java.setFork(true);
java.setClassname("org.apache.tools.ant.Main");
java.setDir(getProject().getBaseDir());
}
private void addProperties (Map propertyMap)
{
for (Iterator propIter = propertyMap.entrySet().iterator();
propIter.hasNext();
)
{
Map.Entry nextProp = (Map.Entry)propIter.next();
java.createArg().setValue("-D" + nextProp.getKey() + "=" + nextProp.getValue());
log("Added property " + nextProp.getKey() + " -> " + nextProp.getValue(), Project.MSG_VERBOSE);
}
}
/**
Execute ant in a separate process according to the current configuration.
@throws BuildException if the forked ant process could not be executed
or if thrown by the Java task
*/
public void execute() throws BuildException
{
javaProject.setInputHandler(getProject().getInputHandler());
for (Iterator listeners = getProject().getBuildListeners().iterator();
listeners.hasNext();
)
{
javaProject.addBuildListener((BuildListener)listeners.next());
}
if (antHome == null)
{
throw new BuildException("antHome must be set");
}
if (!antHome.exists())
{
throw new BuildException("antHome does not exist: " + antHome);
}
Environment.Variable antHomeSysVar = new Environment.Variable();
antHomeSysVar.setKey("ant.home");
antHomeSysVar.setFile(antHome);
java.addSysproperty(antHomeSysVar);
FileSet antLibs = new FileSet();
antLibs.setDir(new File(antHome, "lib"));
antLibs.createInclude().setName("**/*.jar");
Path classpath = createClasspath();
classpath.addFileset(antLibs);
for (Enumeration propEnum = propertyList.elements();
propEnum.hasMoreElements();
)
{
((Property)propEnum.nextElement()).execute();
}
// Put all properties in one Properties object
// Inherited properties first
Properties definedProperties = new Properties();
if (inheritAll)
{
definedProperties.putAll(getProject().getProperties());
log("Inheriting all: " + definedProperties, Project.MSG_DEBUG);
}
else
{
definedProperties.putAll(getProject().getUserProperties());
log("Inheriting user: " + definedProperties, Project.MSG_DEBUG);
}
// Remove predefined properties, as best as possible, so as not to confuse ant
for (Iterator propertyIter = definedProperties.keySet().iterator();
propertyIter.hasNext();
)
{
String name = (String)propertyIter.next();
if (name.startsWith("ant.") || name.startsWith("java.") ||
name.startsWith("os.") || name.startsWith("user.") ||
name.startsWith("sun.") ||
FILTER_PROPERTIES.contains(name))
{
propertyIter.remove();
}
}
// Specified properties second, thus overriding any
// inherited properties
definedProperties.putAll(properties);
log("Explicitly defined: " + properties, Project.MSG_DEBUG);
// Remove basedir or ant will be unable to set it
definedProperties.remove("basedir");
log("All defined properties for antfork: " + definedProperties, Project.MSG_DEBUG);
addProperties(definedProperties);
if (javaHome == null)
{
// Messy to get JAVA_HOME, but this is how other ant tasks do it
Project tempProject = new Project();
Property envProperties = new Property();
envProperties.setProject(tempProject);
envProperties.setEnvironment("env");
envProperties.execute();
javaHome = new File((String)tempProject.getProperty("env.JAVA_HOME"));
tempProject = null;
envProperties = null;
}
if (!javaHome.exists())
{
throw new BuildException("javaHome does not exist: " + javaHome);
}
// Reproducing classpath logic from the ant scripts.
// Add tools.jar and classes.zip as needed.
File javaLibs = new File(javaHome, "lib");
if (javaLibs.exists())
{
File toolsJar = new File(javaLibs, "tools.jar");
if (toolsJar.exists())
{
classpath.createPathElement().setLocation(toolsJar);
}
toolsJar = null;
File classesZip = new File(javaLibs, "classes.zip");
if (classesZip.exists())
{
classpath.createPathElement().setLocation(classesZip);
}
classesZip = null;
}
javaLibs = null;
if (emacs)
{
java.createArg().setValue("-emacs");
}
if (quiet)
{
java.createArg().setValue("-quiet");
}
if (verbose)
{
java.createArg().setValue("-verbose");
}
if (debug)
{
java.createArg().setValue("-debug");
}
if (logfile != null)
{
java.createArg().setValue("-logfile");
try
{
java.createArg().setValue(logfile.getCanonicalPath());
}
catch (IOException ioe)
{
throw new BuildException("Could not determine canonical path for " + logfile, ioe);
}
}
if (logger != null)
{
java.createArg().setValue("-logger");
java.createArg().setValue(logger);
}
if (listener != null)
{
java.createArg().setValue("-listener");
java.createArg().setValue(listener);
}
if (inputHandler != null)
{
java.createArg().setValue("-inputHandler");
java.createArg().setValue(inputHandler);
}
if (buildFile != null)
{
java.createArg().setValue("-buildfile");
java.createArg().setValue(buildFile);
}
if (target != null)
{
java.createArg().setValue(target);
}
// The REAL work
java.execute();
// Null everything to allow garbage collection
javaProject = null;
java = null;
antHome = null;
propertyList = null;
properties = null;
javaHome = null;
logfile = null;
// Request garbage collection so that the underlying Procses
// object doesn't hang around longer than necessary.
System.gc();
}
/**
If true, pass all properties to the new Ant project.
Defaults to <tt>true</tt>.
@param value <tt>true</tt> to inherit the current execution's
properties, <tt>false</tt> to not inherit
*/
public void setInheritAll(boolean value)
{
inheritAll = value;
}
/**
The build file to use.
@param f the ant build file to be processed by the forked ant task
*/
public void setBuildfile(String f)
{
this.buildFile = f;
}
/**
The target of the new Ant project to execute.
Defaults to the new project's default target.
@param s the target in the ant build file to be processed by the
forked ant task
*/
public void setTarget(String s)
{
this.target = s;
}
/**
Set the emacs flag for the forked ant task.
@param b <tt>true</tt> if task labels should be dropped,
<tt>false</tt> to display them
*/
public void setEmacs (boolean b)
{
this.emacs = b;
}
/**
Set the quiet flag for the forked ant task.
@param b <tt>true</tt> if the forked ant task should be quiet,
<tt>false</tt> if it should report messages normally
*/
public void setQuiet (boolean b)
{
this.quiet = b;
}
/**
Set the verbose flag for the forked ant task.
@param b <tt>true</tt> if the forked ant task should be verbose,
<tt>false</tt> if it should report messages normally
*/
public void setVerbose (boolean b)
{
this.verbose = b;
}
/**
Set the debug flag for the forked ant task.
@param b <tt>true</tt> if the forked ant task should be debugged,
<tt>false</tt> if it should report messages normally
*/
public void setDebug (boolean b)
{
this.debug = b;
}
/**
Property to pass to the new project.
The property is passed as a 'user property'
@return a new AntForkProperty
@see AntForkProperty
*/
public Property createProperty()
{
Property property = new AntForkProperty();
property.setTaskName("property");
property.setProject(javaProject);
propertyList.add(property);
return property;
}
/**
Set the ant installation to be run in the new process.
@param f the desired ant installation's location
*/
public void setAntHome (File f)
{
this.antHome = f;
}
/**
Set the Java installation to be run in the new process.
@param f the desired Java installation's location
*/
public void setJavaHome (File f)
{
this.javaHome = f;
}
/**
Set the log file to be used by the forked ant process.
@param f where ant should log its results
*/
public void setLogfile (File f)
{
this.logfile = f;
}
/**
Set the logger to be used by the forked ant process.
@param s the name of the desired logger class
*/
public void setLogger (String s)
{
this.logger = s;
}
/**
Set the event listener to be used by the forked ant process.
@param s the name of the desired event listener class
*/
public void setListener (String s)
{
this.listener = s;
}
/**
Set the input handler to be used by the forked ant process.
@param s the name of the desired input handler class
*/
public void setInputHandler (String s)
{
this.inputHandler = s;
}
/**
Set the classpath to be used for the forked Java execution. This
is the classpath under which ant will be executed.
@param s the Path to be used for the JVM executing ant in the
new process
*/
public void setClasspath(Path s)
{
java.createClasspath().append(s);
}
/**
Creates a nested classpath element to be added to the classpath
for the new process.
@return a new Path that is part of the classpath the Java process
will use for running ant
*/
public Path createClasspath()
{
return java.createClasspath();
}
/**
Adds a reference to a CLASSPATH defined elsewhere.
@param r the classpath to be used by the Java process when running ant
*/
public void setClasspathRef(Reference r)
{
javaProject.addReference(r.getRefId(), r.getReferencedObject(getProject()));
java.createClasspath().setRefid(r);
}
/**
Set the command line arguments for the JVM that will be running ant.
@param s the arguments to be used, delimited by spaces
*/
public void setJvmargs(String s)
{
java.setJvmargs(s);
}
/**
Creates a nested jvmarg element describing a single command line
argument for the JVM that will be running ant.
@return a new command line argument for the JVM
*/
public Commandline.Argument createJvmarg()
{
return java.createJvmarg();
}
/**
Set the command used to start the JVM.
@param s the command to start the JVM
*/
public void setJvm(String s)
{
java.setJvm(s);
}
/**
Add a nested sysproperty element. This value will be visible to ant
in the forked process.
@param sysp the desired system property for the JVM
*/
public void addSysproperty(Environment.Variable sysp)
{
java.addSysproperty(sysp);
}
/**
Throw a BuildException if process returns non 0.
@param fail <tt>true</tt> if an error reported by the forked ant process
should abort the current ant process, <tt>false</tt> if the
current process should continue on
*/
public void setFailonerror(boolean fail)
{
java.setFailonerror(fail);
}
/**
The working directory of the forked ant process
@param d the directory in which the JVM process should be executed
*/
public void setDir(File d)
{
java.setDir(d);
}
/**
File the output of the process is redirected to.
@param out the file where the JVM's output will be captured
*/
public void setOutput(File out)
{
java.setOutput(out);
}
/**
Set the maximum memory that the JVM may use.
@param max the maximum memory to formatted as expected by the JVM
*/
public void setMaxmemory(String max)
{
java.setMaxmemory(max);
}
/**
Specify the Java version to which the JVM complies. This is
currently used by the Java command line support in ant to determine
the maximum memory paramter (-mx for 1.1, -Xmx otherwise).
@param value the Java version supported by the JVM (i.e., "1.3.1")
*/
public void setJVMVersion(String value)
{
java.setJVMVersion(value);
}
/**
Use a completely new environment.
@param newenv <tt>true</tt> if the execution environment should be
duplicated for the forked process, <tt>false</tt> if not
*/
public void setNewenvironment(boolean newenv)
{
java.setNewenvironment(newenv);
}
/**
Add a nested env element - an environment variable.
@param var the environment variable to be set for the forked process
*/
public void addEnv(Environment.Variable var)
{
java.addEnv(var);
}
/**
This subclass of Property is used internally by AntForkTask to capture
all the properties being set for the forked ant task.
*/
private class AntForkProperty extends Property
{
/**
By overriding addProperty, all property name/value pairs are
captured so that they can be saved to a separate file.
@param name the property name
@param value the property value
*/
protected void addProperty (String name, String value)
{
properties.put(name, value);
}
}
}
Wednesday, July 11, 2012
CallOverTask for Ant
I created this callover task many years ago when I was using Ant heavily for many things at home and at work. It was so incredibly useful and flexible that I figured Ant would create something similar in time, but now many years later I still see no such capability. I am putting here what I had written for Ant 1.5. I have not yet checked to see how well this will work with the current version of Ant, 1.8.4. Certainly improvements can be made to take into account the changes between the versions.
There are three types of parameter sets: one generated from a fileset with an optional mapper, one generated from a dirset, and one generated from a delimited list of values.
The above example will run each of the given programs foo, bar, and baz once for each file in the given directory that ends with .out.
Copyright 2005 Steven S. Morgan
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
steven@technicalabilities.com
http://www.technicalabilities.com/java
*/
package com.technicalabilities.ant;
import java.io.*;
import java.util.*;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.taskdefs.CallTarget;
import org.apache.tools.ant.taskdefs.Property;
import org.apache.tools.ant.types.AbstractFileSet;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.DirSet;
import org.apache.tools.ant.types.Mapper;
/**
<p>
The CallOverTask iterates over sets of values calling another task
for each combination of these values.
</p>
<p>
Using explicit values:
</p>
<table border="2" bgcolor="#ffeeee">
<tr><td><strong>Build file</strong></td></tr>
<tr><td>
<pre>
<target name="echoStuff">
<echo message="param1 = ${param1}"/>
</target>
<target name="example1">
<callover target="echoStuff">
<paramset name="param1">
<valueset values="separated,by,commas"/>
</paramset>
</callover>
</target></pre>
</td></tr>
</table>
<table border="2" bgcolor="#ffeeee">
<tr><td><strong>Results</strong></td></tr>
<tr><td>
<pre>
example1:
echoStuff:
[echo] param1 = separated
echoStuff:
[echo] param1 = by
echoStuff:
[echo] param1 = commas
BUILD SUCCESSFUL
Total time: 1 second
</pre>
</td></tr>
</table>
<p>
A FileSet may be used with the ParamSet as well.
</p>
<table border="2" bgcolor="#ffeeee">
<tr><td><strong>Build file</strong></td></tr>
<tr><td>
<pre>
<target name="echoFileNames">
<echo message="filename = ${filename}"/>
</target>
<target name="example2">
<mkdir dir="tmp/subdir"/>
<touch file="tmp/file1.txt"/>
<touch file="tmp/subdir/file2.txt"/>
<callover target="echoFileNames">
<paramset name="filename">
<fileset dir="tmp" includes="**"/>
</paramset>
</callover>
<delete dir="tmp"/>
</target></pre>
</td></tr>
</table>
<table border="2" bgcolor="#ffeeee">
<tr><td><strong>Results</strong></td></tr>
<tr><td>
<pre>
example2:
[mkdir] Created dir: C:\dev\work\ant\tmp\subdir
[touch] Creating C:\dev\work\ant\tmp\file1.txt
[touch] Creating C:\dev\work\ant\tmp\subdir\file2.txt
echoFileNames:
[echo] filename = file1.txt
echoFileNames:
[echo] filename = subdir\file2.txt
[delete] Deleting directory C:\dev\work\ant\tmp
BUILD SUCCESSFUL
Total time: 1 second
</pre>
</td></tr>
</table>
<p>
Multiple parameter sets are possible, as are static parameters, mappers for
file sets, and specified delimiters. When using multiple parameter sets,
every combination of parameters will be performed. This can quickly get
out of hand!
</p>
<table border="2" bgcolor="#ffeeee">
<tr><td><strong>Build file</strong></td></tr>
<tr><td>
<pre>
<target name="echoAll">
<echo message="myValue is ${myValue}"/>
<echo message="filename is ${filename}"/>
<echo message="param1 is ${param1}"/>
</target>
<target name="example3">
<mkdir dir="tmp/subdir"/>
<touch file="tmp/file1.txt"/>
<touch file="tmp/subdir/file2.txt"/>
<callover target="echoAll">
<param name="myValue" value="always the same"/>
<paramset name="filename">
<fileset dir="tmp" includes="**"/>
<mapper type="glob" from="*.txt" to="*"/>
</paramset>
<paramset name="param1">
<valueset values="separated|by|pipes" delimiter="|"/>
</paramset>
</callover>
<delete dir="tmp"/>
</target></pre>
</td></tr>
</table>
<table border="2" bgcolor="#ffeeee">
<tr><td><strong>Results</strong></td></tr>
<tr><td>
<pre>
example3:
[mkdir] Created dir: C:\dev\work\ant\tmp\subdir
[touch] Creating C:\dev\work\ant\tmp\file1.txt
[touch] Creating C:\dev\work\ant\tmp\subdir\file2.txt
echoAll:
[echo] myValue is always the same
[echo] filename is file1
[echo] param1 is separated
echoAll:
[echo] myValue is always the same
[echo] filename is file1
[echo] param1 is by
echoAll:
[echo] myValue is always the same
[echo] filename is file1
[echo] param1 is pipes
echoAll:
[echo] myValue is always the same
[echo] filename is subdir\file2
[echo] param1 is separated
echoAll:
[echo] myValue is always the same
[echo] filename is subdir\file2
[echo] param1 is by
echoAll:
[echo] myValue is always the same
[echo] filename is subdir\file2
[echo] param1 is pipes
[delete] Deleting directory C:\dev\work\ant\tmp
BUILD SUCCESSFUL
Total time: 2 seconds
</pre>
</td></tr>
</table>
<p>
The CallOverTask also supports parallel execution of these tasks.
The <tt>parallel</tt> attribute indicates the number of tasks that
may execute concurrently. Threads are pooled and reused for
efficiency. Note that it is because of this behavior that the
CallOverTask will not abort as a result of errors committed in
the subtasks. The errors are reported via Ant's logging mechanism.
</p>
@author $Author: ssmorgan $
@version $Revision: 1.6 $
*/
public class CallOverTask extends Task
{
/**
The task to be called.
*/
private String target;
/**
Whether or not to inherit properties.
*/
private boolean inheritAll = true;
/**
Whether or not to inherit references.
*/
private boolean inheritRefs = false;
/**
How many tasks may be run simultaneously.
*/
private int parallel = 1;
/**
Static parameters.
*/
private ArrayList params;
/**
Dynamic parameters, the ones from the parameter sets.
*/
private ArrayList paramSets;
/**
Create a new CallOverTask.
*/
public CallOverTask ()
{
params = new ArrayList();
paramSets = new ArrayList();
}
/**
For each combination of parameters in the given <tt><paramset></tt>
tags, call the requested target. This method does not return until
all of these tasks have completed.
@throws BuildException if an error is encountered parsing the XML
(Note that tasks executed as a result of this
<tt><callover></tt> task may generate
their own exceptions, but they will not cause
each other or this task to fail nor will they
cause ant to abort.)
*/
public void execute()
{
if (target == null)
{
throw new BuildException("Attribute target is required.", location);
}
if (paramSets.isEmpty())
{
throw new BuildException("paramSet element required -- otherwise use <antcall>", location);
}
if (parallel < 1)
{
throw new BuildException("Number of parallel threads must be positive", location);
}
TaskThreadPool threadPool = new TaskThreadPool(parallel);
Iterator [] paramSetIters = new Iterator [paramSets.size()];
String [] currentValues = new String [paramSets.size()];
for (int i=0; i<paramSetIters.length; ++i)
{
paramSetIters[i] = ((ParamSet)paramSets.get(i)).values();
if (!paramSetIters[i].hasNext())
{
log("paramset for " + ((ParamSet)paramSets.get(i)).getName() + " is empty.", Project.MSG_WARN);
return;
}
currentValues[i] = (String)paramSetIters[i].next();
}
boolean done = false;
while (!done)
{
CallTarget callee = (CallTarget)project.createTask("antcall");
callee.setOwningTarget(super.target);
callee.setTaskName(getTaskName());
callee.setLocation(location);
callee.init();
callee.setTarget(target);
callee.setInheritAll(inheritAll);
callee.setInheritRefs(inheritRefs);
Iterator paramIter = params.iterator();
while (paramIter.hasNext())
{
Property param = (Property)paramIter.next();
Property p = callee.createParam();
// If only clone() were supported... :)
if (param.getValue() != null)
{
p.setName(param.getName());
p.setValue(param.getValue());
}
else if (param.getFile() != null)
{
if (p.getPrefix() != null)
p.setPrefix(param.getPrefix());
p.setFile(param.getFile());
}
else if (param.getResource() != null)
{
if (p.getPrefix() != null)
p.setPrefix(param.getPrefix());
p.setPrefix(param.getResource());
if (param.getClasspath() != null)
{
p.setClasspath(param.getClasspath());
}
}
else if (param.getEnvironment() != null)
{
p.setPrefix(param.getEnvironment());
}
}
for (int i=0; i<currentValues.length; ++i)
{
Property p = callee.createParam();
p.setName(((ParamSet)paramSets.get(i)).getName());
p.setValue(currentValues[i]);
}
threadPool.getTaskThread().setTask(callee);
boolean incrementPrevious = true;
for (int i=currentValues.length-1; i>=0 && incrementPrevious; --i)
{
incrementPrevious = !paramSetIters[i].hasNext();
if (incrementPrevious)
{
if (i == 0)
{
done = true;
}
else
{
paramSetIters[i] = ((ParamSet)paramSets.get(i)).values();
}
}
if (!done)
currentValues[i] = (String)paramSetIters[i].next();
}
}
threadPool.done();
}
/**
Set the target to be called with each parameter set combination.
There is no default value; this must be set.
@param target the name of the target to be called
*/
public void setTarget(String target)
{
this.target = target;
}
/**
Set the number of tasks that may be run in parallel. The default
value is 1.
@param parallel the maximum number of tasks that may be run in parallel
*/
public void setParallel (int parallel)
{
this.parallel = parallel;
}
/**
Set or clear the flag indicating if properties should be inherited
by the child tasks. The default value is <tt>true</tt>.
@param b <tt>true</tt> to inherit, <tt>false</tt> to not inherit
*/
public void setInheritAll (boolean b)
{
this.inheritAll = b;
}
/**
Set or clear the flag indicating if references should be inherited
by the child tasks. The default value is <tt>false</tt>.
@param b <tt>true</tt> to inherit, <tt>false</tt> to not inherit
*/
public void setInheritRefs (boolean b)
{
this.inheritRefs = b;
}
/**
Like <tt><param></tt> tag for <tt><antcall></tt>, this
creates a Property for use by the subtasks.
@return a new Property
*/
public Property createParam ()
{
Property param = new Property();
params.add(param);
return param;
}
/**
Create a new ParamSet for this CallOverTask.
@return a new ParamSet
*/
public ParamSet createParamSet ()
{
ParamSet paramSet = new ParamSet();
paramSets.add(paramSet);
return paramSet;
}
/**
One of the threads executing a subtask caught something. Report
it via Ant's logging with MSG_ERR priority.
@param t the caught Throwable
@param task the task that generated the caught Throwable
*/
void reportThrowable (Throwable t, Task task)
{
try
{
StringWriter out = new StringWriter();
PrintWriter buf = new PrintWriter(out);
buf.print("Throwable encountered on ");
buf.print(Thread.currentThread().getName());
buf.print(" by ");
buf.println(task.getTaskName());
t.printStackTrace(buf);
buf.flush();
log(out.toString(), Project.MSG_ERR);
buf.close();
out.close();
}
catch (IOException ioe)
{
// highly unlikely that out.close() will throw this, but have to catch it
log(ioe.getMessage(), Project.MSG_WARN);
}
}
/**
A ParamSet defines a set of values over which the CallOverTask
will iterate. A paramset may contain either a ValueSet or a
FileSet with an optional Mapper.
*/
public class ParamSet
{
/**
The name to be assigned the values that this ParamSet will
generate. This is the name that the subtask will find the
value.
*/
private String name;
/**
The set of values over which to iterate as a delimited string.
*/
private ValueSet valueSet;
/**
The set of files over which to iterate. This ParamSet will
generate String values <em>relative to the basedir of the
FileSet</em> with each iteration. Can be a DirSet.
*/
private AbstractFileSet fileSet;
/**
A Mapper for reformatting the filenames generated by the FileSet.
*/
private Mapper filenameMapper;
/**
Set the name by which subtasks will know these values.
@param name a Property name
*/
public void setName (String name)
{
this.name = name;
}
/**
Get the name by which subtasks will know these values.
@return a Property name
*/
String getName ()
{
return name;
}
/**
Create a new ValueSet for this ParamSet.
@return a new ValueSet
*/
public ValueSet createValueSet ()
{
valueSet = new ValueSet();
return valueSet;
}
/**
Add a FileSet to this ParamSet.
@param fileSet the FileSet to be added
*/
public void addFileSet (FileSet fileSet)
{
this.fileSet = fileSet;
}
/**
Add a DirSet to this ParamSet.
@param dirSet the DirSet to be added
*/
public void addDirSet (DirSet dirSet)
{
this.fileSet = dirSet;
}
/**
Create a new Mapper for this ParamSet.
@return a new Mapper
@throws BuildException if a Mapper has already been defined
*/
public Mapper createMapper () throws BuildException
{
if (filenameMapper != null)
{
throw new BuildException("Cannot define more than one mapper", location);
}
filenameMapper = new Mapper(project);
return filenameMapper;
}
/**
Obtain an Iterator for the values represented by this ParamSet.
@return an Iterator that will provide each value from this ParamSet
*/
Iterator values()
{
if (valueSet != null)
{
if (filenameMapper != null)
throw new BuildException("paramset does not use a mapper with a valueset", location);
// When using a ValueSet, return its Iterator.
return valueSet.values();
}
if (fileSet != null)
{
if (filenameMapper == null)
{
/**
This Iterator is created when there is a FileSet
without a Mapper.
*/
return new Iterator ()
{
/**
The files known to the FileSet
*/
private String [] files = (fileSet instanceof FileSet)
? fileSet.getDirectoryScanner(project).getIncludedFiles()
: fileSet.getDirectoryScanner(project).getIncludedDirectories();
/**
Internal index
*/
private int i=0;
/**
Show if values still remain.
@return <tt>true</tt> if values still remain,
<tt>false</tt> if not
*/
public boolean hasNext ()
{
return i < files.length;
}
/**
Return the next value in the FileSet.
Note that this will be relative to the
FileSet's basedir.
@return the next value in the FileSet
*/
public Object next ()
{
return files[i++];
}
/**
Unsupported by this Iterator.
@throws UnsupportedOperationException always
*/
public void remove ()
{
throw new UnsupportedOperationException();
}
};
}
else
{
/**
This Iterator is created when a Mapper is present
with a FileSet.
*/
return new Iterator ()
{
/**
The files known to this FileSet.
*/
private String [] files = (fileSet instanceof FileSet)
? fileSet.getDirectoryScanner(project).getIncludedFiles()
: fileSet.getDirectoryScanner(project).getIncludedDirectories();
/**
Internal index
*/
private int i=0;
/**
The names generated by the Mapper for the
given set of files.
*/
private String [] mappedNames = new String [0];
/**
Internal index
*/
private int j=0;
/**
The next value to be returned by this Iterator
*/
private String nextValue;
/**
Constructor sets the first value.
*/
{
setNextValue();
}
/**
The logic here is a bit funky because a
FileNameMapper can return multiple mappings
for a single file. As long as mappings
remain for the current file, return those
mappings. Once those mappings are exhausted,
then go the mappings for the next file.
Once all the files have been exhausted, then
we are done.
*/
private void setNextValue ()
{
nextValue = null;
while (j < mappedNames.length && nextValue == null)
{
nextValue = mappedNames[j];
++j;
}
if (nextValue == null)
{
while (i < files.length && nextValue == null)
{
mappedNames = filenameMapper.getImplementation().mapFileName(files[i]);
++i;
if (mappedNames != null)
{
j = 0;
setNextValue();
}
}
}
}
/**
Show if values still remain.
@return <tt>true</tt> if values still remain,
<tt>false</tt> if not
*/
public boolean hasNext ()
{
return nextValue != null;
}
/**
Return the next value.
@return the next value from the mapped FileSet
*/
public Object next ()
{
String value = nextValue;
setNextValue();
return value;
}
/**
Unsupported by this Iterator.
@throws UnsupportedOperationException always
*/
public void remove ()
{
throw new UnsupportedOperationException();
}
};
}
}
throw new BuildException("paramset must have a valueset, a fileset, or a dirset", location);
}
}
/**
A ValueSet parses a delimited String.
*/
public class ValueSet
{
/**
The delimited String
*/
private String values;
/**
The value delimiter
*/
private String delimiter=",";
/**
Set the delimited String of values. There is no
default value; this must be set.
@param values the values as a delimited String
*/
public void setValues (String values)
{
this.values = values;
}
/**
Set the value delimiter. The default value is the comma (,).
@param delimiter the value delimiter
*/
public void setDelimiter (String delimiter)
{
this.delimiter = delimiter;
}
/**
Provide an Iterator for accessing the values represented
by this ValueSet.
@return an Iterator that iterates over the defined values
*/
Iterator values ()
{
/**
This Iterator is created when a ParamSet is defined by
a ValueSet.
*/
return new Iterator ()
{
/**
The Iterator simply wraps a StringTokenizer.
*/
private StringTokenizer tok = new StringTokenizer(values, delimiter);
/**
Show if values still remain.
@return <tt>true</tt> if values still remain,
<tt>false</tt> if not
*/
public boolean hasNext ()
{
return tok.hasMoreTokens();
}
/**
Return the next value.
@return the next value from the ValueSet
*/
public Object next ()
{
return tok.nextToken();
}
/**
Unsupported by this Iterator.
@throws UnsupportedOperationException always
*/
public void remove ()
{
throw new UnsupportedOperationException();
}
};
}
}
/**
A thread pool provides an optimal implementation for supporting
multiple parallel threads with minimal overhead.
*/
private class TaskThreadPool
{
/**
The pool of available TaskThreads
*/
private ArrayList pool;
/**
A list of all TaskThreads managed by this pool
*/
private ArrayList references;
/**
Create a new TaskThreadPool with the given number
of available TaskThreads.
@param parallel the number of available TaskThreads
*/
TaskThreadPool (int parallel)
{
this.pool = new ArrayList(parallel);
for (int i=0; i<parallel; ++i)
{
this.pool.add(new TaskThread(this, i));
}
this.references = new ArrayList(this.pool);
}
/**
Get the next available TaskThread. This method blocks the
calling Thread until a TaskThread is available.
@return the next available TaskThread
*/
synchronized TaskThread getTaskThread ()
{
while (pool.isEmpty())
{
try
{
wait();
}
catch (InterruptedException goOn) {}
}
return (TaskThread)pool.remove(0);
}
/**
When a TaskThread comletes its task, it returns to
the pool here.
@param taskThread the newly-available TaskThread
*/
synchronized void taskComplete (TaskThread taskThread)
{
pool.add(taskThread);
notify();
System.gc();
}
/**
The CallOverTask calls done() on the TaskThreadPool when
it is done handing out tasks. This method notifies the
TaskThreads and then calls join() on all the TaskThreads.
As a result, this method will not return until all the
TaskThreads have completed their tasks.
*/
void done ()
{
for (Iterator refs = references.iterator(); refs.hasNext();)
{
TaskThread nextThread = (TaskThread)refs.next();
nextThread.done();
try
{
nextThread.join();
}
catch (InterruptedException goOn) {}
}
}
}
/**
A TaskThread is a resuable Thread that processes tasks handed
out by the CallOverTask.
*/
private class TaskThread extends Thread
{
/**
The task to be performed
*/
private Task task;
/**
Flag indicating if the CallOverTask is finished handing
out tasks
*/
private boolean done;
/**
The TaskThreadPool managing this TaskThread
*/
private TaskThreadPool pool;
/**
This TaskThread's number (for logging, mainly)
*/
private int threadNumber;
/**
Create a new TaskThread.
@param pool the managing TaskThreadPool
@param threadNumber this TaskThread's number (used to set
the Thread's name)
*/
TaskThread (TaskThreadPool pool, int threadNumber)
{
this.done = false;
this.pool = pool;
this.threadNumber = threadNumber;
setName("callover(" + Integer.toHexString(CallOverTask.this.hashCode()) + ") thread-" + threadNumber);
start();
}
/**
Set this task's Thread. The TaskThread immediately goes
to work on this task in its own Thread.
@param task the task to be performed
*/
synchronized void setTask (Task task)
{
this.task = task;
if (task != null)
{
notify();
}
}
/**
Tell this TaskThread that the CallOverTask is done handing
out tasks. When the current task, if any, is complete, then
the run() method will exit.
*/
synchronized void done ()
{
this.done = true;
notify();
}
/**
Needed a synchronized accessor so that the calling Thread
would by synch'ed up with the other Threads. Test for both
done and for no task assignment.
@return <tt>true</tt> if the CallOverTask is done handing
out tasks, <tt>false</tt> otherwise
*/
private synchronized boolean isDone ()
{
return this.done && this.task == null;
}
/**
Performs all assigned tasks until the CallOverTask says
it is done. Any Throwables caught are relayed through
Ant's error log.
@see com.technicalabilities.ant.CallOverTask#reportThrowable(java.lang.Throwable,org.apache.tools.ant.Task)
*/
public void run ()
{
while (!isDone())
{
Task myTask = null;
synchronized (this)
{
while (task == null && !done)
{
try
{
wait();
}
catch (InterruptedException goOn) {}
}
myTask = this.task;
}
if (myTask != null)
{
try
{
myTask.perform();
}
catch (Throwable t)
{
reportThrowable(t, myTask);
}
setTask(null);
pool.taskComplete(this);
}
}
}
}
}
Iterating Over A Set of Parameter Values
The way it works is that callover takes a set of parameters, a property name, and a target in the current Ant environment. The target is called (like <antcall>) in the ant file once for each parameter value in the set with the specified property name set to that value.There are three types of parameter sets: one generated from a fileset with an optional mapper, one generated from a dirset, and one generated from a delimited list of values.
Using a FileSet
This example executes an external command for every text file in a directory hierarchy.<target name="fooOver">
<callover target="doFoo" inheritAll="false">
<paramset name="file.name">
<fileset dir="${root.dir}" includes="**/*.txt">
<globmapper from="*.txt" to="*"/>
</fileset>
</paramset>
</callover>
</target>
<target name="doFoo">
<exec executable="foo">
<arg value="${file.name}.txt"/>
<arg value="${file.name}.out"/>
</exec>
</target>
Using a DirSet
This example iterates over a set of directories with a common structure and performs a clean on each one.<target name="cleanall"> <callover target="clean" inheritAll="false"> <paramset name="module.dir"> <dirset dir="${module.root}" includes="*"/> </paramset> </callover> </target> <target name="clean"> <delete dir="${module.dir}/dist" failonerror="no"/> <delete dir="${module.dir}/test/results" failonerror="no"/> <delete dir="${module.dir}/test/plain/classes" failonerror="no"/> <delete dir="${module.dir}/test/ejb/classes" failonerror="no"/> <delete dir="${module.dir}/test/web/classes" failonerror="no"/> <delete> <fileset dir="${module.dir}" includes="**/*.class"/> </delete> </target>
Using a ValueSet
This example echoes each value one at a time.<target name="processInParallel"> <callover target="process" parallel="3">
<paramset name="greek.letter">
<valueset values="alpha beta gamma delta epsilon" delimiter=" "/>
</paramset>
</callover> </target>
<target name="process">
<echo message="Current letter is ${greek.letter}."/>
</target>
The Real Power: Combinations
The best part of <callover> is that if multiple parameter sets are provided, then the target task is called once for every combination of parameter values. (Hopefully you don't have too many combinations!)<target name="postProcess">
<callover target="doPostProcess" inheritAll="false">
<paramset name="program"> <valueset values="foo bar baz" delimiter=" "/> </paramset>
<paramset name="file.name">
<fileset dir="${root.dir}" includes="*.out"/>
<paramset>
</callover>
</target>
<target name="doPostProcess">
<exec executable="${program}">
<arg value="${file.name}"/>
</exec>
</target>
The above example will run each of the given programs foo, bar, and baz once for each file in the given directory that ends with .out.
The Code
/*Copyright 2005 Steven S. Morgan
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
steven@technicalabilities.com
http://www.technicalabilities.com/java
*/
package com.technicalabilities.ant;
import java.io.*;
import java.util.*;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.taskdefs.CallTarget;
import org.apache.tools.ant.taskdefs.Property;
import org.apache.tools.ant.types.AbstractFileSet;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.DirSet;
import org.apache.tools.ant.types.Mapper;
/**
<p>
The CallOverTask iterates over sets of values calling another task
for each combination of these values.
</p>
<p>
Using explicit values:
</p>
<table border="2" bgcolor="#ffeeee">
<tr><td><strong>Build file</strong></td></tr>
<tr><td>
<pre>
<target name="echoStuff">
<echo message="param1 = ${param1}"/>
</target>
<target name="example1">
<callover target="echoStuff">
<paramset name="param1">
<valueset values="separated,by,commas"/>
</paramset>
</callover>
</target></pre>
</td></tr>
</table>
<table border="2" bgcolor="#ffeeee">
<tr><td><strong>Results</strong></td></tr>
<tr><td>
<pre>
example1:
echoStuff:
[echo] param1 = separated
echoStuff:
[echo] param1 = by
echoStuff:
[echo] param1 = commas
BUILD SUCCESSFUL
Total time: 1 second
</pre>
</td></tr>
</table>
<p>
A FileSet may be used with the ParamSet as well.
</p>
<table border="2" bgcolor="#ffeeee">
<tr><td><strong>Build file</strong></td></tr>
<tr><td>
<pre>
<target name="echoFileNames">
<echo message="filename = ${filename}"/>
</target>
<target name="example2">
<mkdir dir="tmp/subdir"/>
<touch file="tmp/file1.txt"/>
<touch file="tmp/subdir/file2.txt"/>
<callover target="echoFileNames">
<paramset name="filename">
<fileset dir="tmp" includes="**"/>
</paramset>
</callover>
<delete dir="tmp"/>
</target></pre>
</td></tr>
</table>
<table border="2" bgcolor="#ffeeee">
<tr><td><strong>Results</strong></td></tr>
<tr><td>
<pre>
example2:
[mkdir] Created dir: C:\dev\work\ant\tmp\subdir
[touch] Creating C:\dev\work\ant\tmp\file1.txt
[touch] Creating C:\dev\work\ant\tmp\subdir\file2.txt
echoFileNames:
[echo] filename = file1.txt
echoFileNames:
[echo] filename = subdir\file2.txt
[delete] Deleting directory C:\dev\work\ant\tmp
BUILD SUCCESSFUL
Total time: 1 second
</pre>
</td></tr>
</table>
<p>
Multiple parameter sets are possible, as are static parameters, mappers for
file sets, and specified delimiters. When using multiple parameter sets,
every combination of parameters will be performed. This can quickly get
out of hand!
</p>
<table border="2" bgcolor="#ffeeee">
<tr><td><strong>Build file</strong></td></tr>
<tr><td>
<pre>
<target name="echoAll">
<echo message="myValue is ${myValue}"/>
<echo message="filename is ${filename}"/>
<echo message="param1 is ${param1}"/>
</target>
<target name="example3">
<mkdir dir="tmp/subdir"/>
<touch file="tmp/file1.txt"/>
<touch file="tmp/subdir/file2.txt"/>
<callover target="echoAll">
<param name="myValue" value="always the same"/>
<paramset name="filename">
<fileset dir="tmp" includes="**"/>
<mapper type="glob" from="*.txt" to="*"/>
</paramset>
<paramset name="param1">
<valueset values="separated|by|pipes" delimiter="|"/>
</paramset>
</callover>
<delete dir="tmp"/>
</target></pre>
</td></tr>
</table>
<table border="2" bgcolor="#ffeeee">
<tr><td><strong>Results</strong></td></tr>
<tr><td>
<pre>
example3:
[mkdir] Created dir: C:\dev\work\ant\tmp\subdir
[touch] Creating C:\dev\work\ant\tmp\file1.txt
[touch] Creating C:\dev\work\ant\tmp\subdir\file2.txt
echoAll:
[echo] myValue is always the same
[echo] filename is file1
[echo] param1 is separated
echoAll:
[echo] myValue is always the same
[echo] filename is file1
[echo] param1 is by
echoAll:
[echo] myValue is always the same
[echo] filename is file1
[echo] param1 is pipes
echoAll:
[echo] myValue is always the same
[echo] filename is subdir\file2
[echo] param1 is separated
echoAll:
[echo] myValue is always the same
[echo] filename is subdir\file2
[echo] param1 is by
echoAll:
[echo] myValue is always the same
[echo] filename is subdir\file2
[echo] param1 is pipes
[delete] Deleting directory C:\dev\work\ant\tmp
BUILD SUCCESSFUL
Total time: 2 seconds
</pre>
</td></tr>
</table>
<p>
The CallOverTask also supports parallel execution of these tasks.
The <tt>parallel</tt> attribute indicates the number of tasks that
may execute concurrently. Threads are pooled and reused for
efficiency. Note that it is because of this behavior that the
CallOverTask will not abort as a result of errors committed in
the subtasks. The errors are reported via Ant's logging mechanism.
</p>
@author $Author: ssmorgan $
@version $Revision: 1.6 $
*/
public class CallOverTask extends Task
{
/**
The task to be called.
*/
private String target;
/**
Whether or not to inherit properties.
*/
private boolean inheritAll = true;
/**
Whether or not to inherit references.
*/
private boolean inheritRefs = false;
/**
How many tasks may be run simultaneously.
*/
private int parallel = 1;
/**
Static parameters.
*/
private ArrayList params;
/**
Dynamic parameters, the ones from the parameter sets.
*/
private ArrayList paramSets;
/**
Create a new CallOverTask.
*/
public CallOverTask ()
{
params = new ArrayList();
paramSets = new ArrayList();
}
/**
For each combination of parameters in the given <tt><paramset></tt>
tags, call the requested target. This method does not return until
all of these tasks have completed.
@throws BuildException if an error is encountered parsing the XML
(Note that tasks executed as a result of this
<tt><callover></tt> task may generate
their own exceptions, but they will not cause
each other or this task to fail nor will they
cause ant to abort.)
*/
public void execute()
{
if (target == null)
{
throw new BuildException("Attribute target is required.", location);
}
if (paramSets.isEmpty())
{
throw new BuildException("paramSet element required -- otherwise use <antcall>", location);
}
if (parallel < 1)
{
throw new BuildException("Number of parallel threads must be positive", location);
}
TaskThreadPool threadPool = new TaskThreadPool(parallel);
Iterator [] paramSetIters = new Iterator [paramSets.size()];
String [] currentValues = new String [paramSets.size()];
for (int i=0; i<paramSetIters.length; ++i)
{
paramSetIters[i] = ((ParamSet)paramSets.get(i)).values();
if (!paramSetIters[i].hasNext())
{
log("paramset for " + ((ParamSet)paramSets.get(i)).getName() + " is empty.", Project.MSG_WARN);
return;
}
currentValues[i] = (String)paramSetIters[i].next();
}
boolean done = false;
while (!done)
{
CallTarget callee = (CallTarget)project.createTask("antcall");
callee.setOwningTarget(super.target);
callee.setTaskName(getTaskName());
callee.setLocation(location);
callee.init();
callee.setTarget(target);
callee.setInheritAll(inheritAll);
callee.setInheritRefs(inheritRefs);
Iterator paramIter = params.iterator();
while (paramIter.hasNext())
{
Property param = (Property)paramIter.next();
Property p = callee.createParam();
// If only clone() were supported... :)
if (param.getValue() != null)
{
p.setName(param.getName());
p.setValue(param.getValue());
}
else if (param.getFile() != null)
{
if (p.getPrefix() != null)
p.setPrefix(param.getPrefix());
p.setFile(param.getFile());
}
else if (param.getResource() != null)
{
if (p.getPrefix() != null)
p.setPrefix(param.getPrefix());
p.setPrefix(param.getResource());
if (param.getClasspath() != null)
{
p.setClasspath(param.getClasspath());
}
}
else if (param.getEnvironment() != null)
{
p.setPrefix(param.getEnvironment());
}
}
for (int i=0; i<currentValues.length; ++i)
{
Property p = callee.createParam();
p.setName(((ParamSet)paramSets.get(i)).getName());
p.setValue(currentValues[i]);
}
threadPool.getTaskThread().setTask(callee);
boolean incrementPrevious = true;
for (int i=currentValues.length-1; i>=0 && incrementPrevious; --i)
{
incrementPrevious = !paramSetIters[i].hasNext();
if (incrementPrevious)
{
if (i == 0)
{
done = true;
}
else
{
paramSetIters[i] = ((ParamSet)paramSets.get(i)).values();
}
}
if (!done)
currentValues[i] = (String)paramSetIters[i].next();
}
}
threadPool.done();
}
/**
Set the target to be called with each parameter set combination.
There is no default value; this must be set.
@param target the name of the target to be called
*/
public void setTarget(String target)
{
this.target = target;
}
/**
Set the number of tasks that may be run in parallel. The default
value is 1.
@param parallel the maximum number of tasks that may be run in parallel
*/
public void setParallel (int parallel)
{
this.parallel = parallel;
}
/**
Set or clear the flag indicating if properties should be inherited
by the child tasks. The default value is <tt>true</tt>.
@param b <tt>true</tt> to inherit, <tt>false</tt> to not inherit
*/
public void setInheritAll (boolean b)
{
this.inheritAll = b;
}
/**
Set or clear the flag indicating if references should be inherited
by the child tasks. The default value is <tt>false</tt>.
@param b <tt>true</tt> to inherit, <tt>false</tt> to not inherit
*/
public void setInheritRefs (boolean b)
{
this.inheritRefs = b;
}
/**
Like <tt><param></tt> tag for <tt><antcall></tt>, this
creates a Property for use by the subtasks.
@return a new Property
*/
public Property createParam ()
{
Property param = new Property();
params.add(param);
return param;
}
/**
Create a new ParamSet for this CallOverTask.
@return a new ParamSet
*/
public ParamSet createParamSet ()
{
ParamSet paramSet = new ParamSet();
paramSets.add(paramSet);
return paramSet;
}
/**
One of the threads executing a subtask caught something. Report
it via Ant's logging with MSG_ERR priority.
@param t the caught Throwable
@param task the task that generated the caught Throwable
*/
void reportThrowable (Throwable t, Task task)
{
try
{
StringWriter out = new StringWriter();
PrintWriter buf = new PrintWriter(out);
buf.print("Throwable encountered on ");
buf.print(Thread.currentThread().getName());
buf.print(" by ");
buf.println(task.getTaskName());
t.printStackTrace(buf);
buf.flush();
log(out.toString(), Project.MSG_ERR);
buf.close();
out.close();
}
catch (IOException ioe)
{
// highly unlikely that out.close() will throw this, but have to catch it
log(ioe.getMessage(), Project.MSG_WARN);
}
}
/**
A ParamSet defines a set of values over which the CallOverTask
will iterate. A paramset may contain either a ValueSet or a
FileSet with an optional Mapper.
*/
public class ParamSet
{
/**
The name to be assigned the values that this ParamSet will
generate. This is the name that the subtask will find the
value.
*/
private String name;
/**
The set of values over which to iterate as a delimited string.
*/
private ValueSet valueSet;
/**
The set of files over which to iterate. This ParamSet will
generate String values <em>relative to the basedir of the
FileSet</em> with each iteration. Can be a DirSet.
*/
private AbstractFileSet fileSet;
/**
A Mapper for reformatting the filenames generated by the FileSet.
*/
private Mapper filenameMapper;
/**
Set the name by which subtasks will know these values.
@param name a Property name
*/
public void setName (String name)
{
this.name = name;
}
/**
Get the name by which subtasks will know these values.
@return a Property name
*/
String getName ()
{
return name;
}
/**
Create a new ValueSet for this ParamSet.
@return a new ValueSet
*/
public ValueSet createValueSet ()
{
valueSet = new ValueSet();
return valueSet;
}
/**
Add a FileSet to this ParamSet.
@param fileSet the FileSet to be added
*/
public void addFileSet (FileSet fileSet)
{
this.fileSet = fileSet;
}
/**
Add a DirSet to this ParamSet.
@param dirSet the DirSet to be added
*/
public void addDirSet (DirSet dirSet)
{
this.fileSet = dirSet;
}
/**
Create a new Mapper for this ParamSet.
@return a new Mapper
@throws BuildException if a Mapper has already been defined
*/
public Mapper createMapper () throws BuildException
{
if (filenameMapper != null)
{
throw new BuildException("Cannot define more than one mapper", location);
}
filenameMapper = new Mapper(project);
return filenameMapper;
}
/**
Obtain an Iterator for the values represented by this ParamSet.
@return an Iterator that will provide each value from this ParamSet
*/
Iterator values()
{
if (valueSet != null)
{
if (filenameMapper != null)
throw new BuildException("paramset does not use a mapper with a valueset", location);
// When using a ValueSet, return its Iterator.
return valueSet.values();
}
if (fileSet != null)
{
if (filenameMapper == null)
{
/**
This Iterator is created when there is a FileSet
without a Mapper.
*/
return new Iterator ()
{
/**
The files known to the FileSet
*/
private String [] files = (fileSet instanceof FileSet)
? fileSet.getDirectoryScanner(project).getIncludedFiles()
: fileSet.getDirectoryScanner(project).getIncludedDirectories();
/**
Internal index
*/
private int i=0;
/**
Show if values still remain.
@return <tt>true</tt> if values still remain,
<tt>false</tt> if not
*/
public boolean hasNext ()
{
return i < files.length;
}
/**
Return the next value in the FileSet.
Note that this will be relative to the
FileSet's basedir.
@return the next value in the FileSet
*/
public Object next ()
{
return files[i++];
}
/**
Unsupported by this Iterator.
@throws UnsupportedOperationException always
*/
public void remove ()
{
throw new UnsupportedOperationException();
}
};
}
else
{
/**
This Iterator is created when a Mapper is present
with a FileSet.
*/
return new Iterator ()
{
/**
The files known to this FileSet.
*/
private String [] files = (fileSet instanceof FileSet)
? fileSet.getDirectoryScanner(project).getIncludedFiles()
: fileSet.getDirectoryScanner(project).getIncludedDirectories();
/**
Internal index
*/
private int i=0;
/**
The names generated by the Mapper for the
given set of files.
*/
private String [] mappedNames = new String [0];
/**
Internal index
*/
private int j=0;
/**
The next value to be returned by this Iterator
*/
private String nextValue;
/**
Constructor sets the first value.
*/
{
setNextValue();
}
/**
The logic here is a bit funky because a
FileNameMapper can return multiple mappings
for a single file. As long as mappings
remain for the current file, return those
mappings. Once those mappings are exhausted,
then go the mappings for the next file.
Once all the files have been exhausted, then
we are done.
*/
private void setNextValue ()
{
nextValue = null;
while (j < mappedNames.length && nextValue == null)
{
nextValue = mappedNames[j];
++j;
}
if (nextValue == null)
{
while (i < files.length && nextValue == null)
{
mappedNames = filenameMapper.getImplementation().mapFileName(files[i]);
++i;
if (mappedNames != null)
{
j = 0;
setNextValue();
}
}
}
}
/**
Show if values still remain.
@return <tt>true</tt> if values still remain,
<tt>false</tt> if not
*/
public boolean hasNext ()
{
return nextValue != null;
}
/**
Return the next value.
@return the next value from the mapped FileSet
*/
public Object next ()
{
String value = nextValue;
setNextValue();
return value;
}
/**
Unsupported by this Iterator.
@throws UnsupportedOperationException always
*/
public void remove ()
{
throw new UnsupportedOperationException();
}
};
}
}
throw new BuildException("paramset must have a valueset, a fileset, or a dirset", location);
}
}
/**
A ValueSet parses a delimited String.
*/
public class ValueSet
{
/**
The delimited String
*/
private String values;
/**
The value delimiter
*/
private String delimiter=",";
/**
Set the delimited String of values. There is no
default value; this must be set.
@param values the values as a delimited String
*/
public void setValues (String values)
{
this.values = values;
}
/**
Set the value delimiter. The default value is the comma (,).
@param delimiter the value delimiter
*/
public void setDelimiter (String delimiter)
{
this.delimiter = delimiter;
}
/**
Provide an Iterator for accessing the values represented
by this ValueSet.
@return an Iterator that iterates over the defined values
*/
Iterator values ()
{
/**
This Iterator is created when a ParamSet is defined by
a ValueSet.
*/
return new Iterator ()
{
/**
The Iterator simply wraps a StringTokenizer.
*/
private StringTokenizer tok = new StringTokenizer(values, delimiter);
/**
Show if values still remain.
@return <tt>true</tt> if values still remain,
<tt>false</tt> if not
*/
public boolean hasNext ()
{
return tok.hasMoreTokens();
}
/**
Return the next value.
@return the next value from the ValueSet
*/
public Object next ()
{
return tok.nextToken();
}
/**
Unsupported by this Iterator.
@throws UnsupportedOperationException always
*/
public void remove ()
{
throw new UnsupportedOperationException();
}
};
}
}
/**
A thread pool provides an optimal implementation for supporting
multiple parallel threads with minimal overhead.
*/
private class TaskThreadPool
{
/**
The pool of available TaskThreads
*/
private ArrayList pool;
/**
A list of all TaskThreads managed by this pool
*/
private ArrayList references;
/**
Create a new TaskThreadPool with the given number
of available TaskThreads.
@param parallel the number of available TaskThreads
*/
TaskThreadPool (int parallel)
{
this.pool = new ArrayList(parallel);
for (int i=0; i<parallel; ++i)
{
this.pool.add(new TaskThread(this, i));
}
this.references = new ArrayList(this.pool);
}
/**
Get the next available TaskThread. This method blocks the
calling Thread until a TaskThread is available.
@return the next available TaskThread
*/
synchronized TaskThread getTaskThread ()
{
while (pool.isEmpty())
{
try
{
wait();
}
catch (InterruptedException goOn) {}
}
return (TaskThread)pool.remove(0);
}
/**
When a TaskThread comletes its task, it returns to
the pool here.
@param taskThread the newly-available TaskThread
*/
synchronized void taskComplete (TaskThread taskThread)
{
pool.add(taskThread);
notify();
System.gc();
}
/**
The CallOverTask calls done() on the TaskThreadPool when
it is done handing out tasks. This method notifies the
TaskThreads and then calls join() on all the TaskThreads.
As a result, this method will not return until all the
TaskThreads have completed their tasks.
*/
void done ()
{
for (Iterator refs = references.iterator(); refs.hasNext();)
{
TaskThread nextThread = (TaskThread)refs.next();
nextThread.done();
try
{
nextThread.join();
}
catch (InterruptedException goOn) {}
}
}
}
/**
A TaskThread is a resuable Thread that processes tasks handed
out by the CallOverTask.
*/
private class TaskThread extends Thread
{
/**
The task to be performed
*/
private Task task;
/**
Flag indicating if the CallOverTask is finished handing
out tasks
*/
private boolean done;
/**
The TaskThreadPool managing this TaskThread
*/
private TaskThreadPool pool;
/**
This TaskThread's number (for logging, mainly)
*/
private int threadNumber;
/**
Create a new TaskThread.
@param pool the managing TaskThreadPool
@param threadNumber this TaskThread's number (used to set
the Thread's name)
*/
TaskThread (TaskThreadPool pool, int threadNumber)
{
this.done = false;
this.pool = pool;
this.threadNumber = threadNumber;
setName("callover(" + Integer.toHexString(CallOverTask.this.hashCode()) + ") thread-" + threadNumber);
start();
}
/**
Set this task's Thread. The TaskThread immediately goes
to work on this task in its own Thread.
@param task the task to be performed
*/
synchronized void setTask (Task task)
{
this.task = task;
if (task != null)
{
notify();
}
}
/**
Tell this TaskThread that the CallOverTask is done handing
out tasks. When the current task, if any, is complete, then
the run() method will exit.
*/
synchronized void done ()
{
this.done = true;
notify();
}
/**
Needed a synchronized accessor so that the calling Thread
would by synch'ed up with the other Threads. Test for both
done and for no task assignment.
@return <tt>true</tt> if the CallOverTask is done handing
out tasks, <tt>false</tt> otherwise
*/
private synchronized boolean isDone ()
{
return this.done && this.task == null;
}
/**
Performs all assigned tasks until the CallOverTask says
it is done. Any Throwables caught are relayed through
Ant's error log.
@see com.technicalabilities.ant.CallOverTask#reportThrowable(java.lang.Throwable,org.apache.tools.ant.Task)
*/
public void run ()
{
while (!isDone())
{
Task myTask = null;
synchronized (this)
{
while (task == null && !done)
{
try
{
wait();
}
catch (InterruptedException goOn) {}
}
myTask = this.task;
}
if (myTask != null)
{
try
{
myTask.perform();
}
catch (Throwable t)
{
reportThrowable(t, myTask);
}
setTask(null);
pool.taskComplete(this);
}
}
}
}
}
Subscribe to:
Posts (Atom)