/********************************************************************************
* CruiseControl, a Continuous Integration Toolkit
* Copyright (c) 2003, ThoughtWorks, Inc.
* 200 E. Randolph, 25th Floor
* Chicago, IL 60601 USA
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* + Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* + Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* + Neither the name of ThoughtWorks, Inc., CruiseControl, nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
********************************************************************************/
package net.sourceforge.cruisecontrol.builders;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.io.File;
import net.sourceforge.cruisecontrol.CruiseControlException;
import net.sourceforge.cruisecontrol.Progress;
import net.sourceforge.cruisecontrol.util.Commandline;
import net.sourceforge.cruisecontrol.util.OSEnvironment;
import net.sourceforge.cruisecontrol.util.StreamConsumer;
import net.sourceforge.cruisecontrol.util.UtilLocator;
import org.apache.log4j.Logger;
/**
* Ant script class.
*
* Contains all the details related to running a Ant based build via
* either a batch script or inprocess.
* @author <a href="mailto:epugh@opensourceconnections.com">Eric Pugh</a>
*/
public class AntScript implements Script, StreamConsumer {
private static final Logger LOG = Logger.getLogger(AntScript.class);
static final String CLASSNAME_DASHBOARD_LISTENER
= "net.sourceforge.cruisecontrol.builders.AntOutputLogger";
static final String CLASSNAME_ANTPROGRESS_LOGGER
= "net.sourceforge.cruisecontrol.builders.AntProgressLogger";
static final String CLASSNAME_ANTPROGRESS_XML_LOGGER
= "net.sourceforge.cruisecontrol.builders.AntProgressXmlLogger";
static final String CLASSNAME_ANTPROGRESS_XML_LISTENER
= "net.sourceforge.cruisecontrol.builders.AntProgressXmlListener";
public static final String LIBNAME_PROGRESS_LOGGER = "cruisecontrol-antprogresslogger.jar";
/**
* Prefix prepended to system out messages to be detected by AntScript as progress messages.
* NOTE: Must be the exact same string as that defined in AntProgressLog constant, kept separate
* to avoid dependence on Ant Builder classes in AntScript.
*/
static final String MSG_PREFIX_ANT_PROGRESS = "ccAntProgress -- ";
private Map<String, String> buildProperties;
private boolean isWindows;
private String antScript;
private List<AntBuilder.JVMArg> args;
private List<AntBuilder.Lib> libs;
private List<AntBuilder.Listener> listeners;
private String loggerClassName;
private boolean isLoggerClassNameSet;
private boolean showAntOutput;
private String tempFileName = "log.xml";
private boolean useScript;
private boolean useLogger;
private boolean useQuiet;
private boolean useDebug;
private boolean keepGoing;
private String buildFile = "build.xml";
private List<Property> properties;
private String target = "";
private String systemClassPath;
private int exitCode;
private String propertyfile;
private String progressLoggerLib;
private Progress progress;
private OSEnvironment env;
/**
* construct the command that we're going to execute.
*
* @return Commandline holding command to be executed
* @throws CruiseControlException on unquotable attributes
*/
public Commandline buildCommandline() throws CruiseControlException {
final Commandline cmdLine = new Commandline();
if (useScript) {
cmdLine.setExecutable(antScript);
} else {
if (isWindows) {
cmdLine.setExecutable("java.exe");
} else {
cmdLine.setExecutable("java");
}
for (final AntBuilder.JVMArg jvmArg : args) {
final String arg = jvmArg.getArg();
// empty args may break the command line
if (arg != null && arg.length() > 0) {
cmdLine.createArgument(arg);
}
}
final List<String> classpathItems = getClasspathItems(systemClassPath, isWindows);
final String antLauncherJarLocation = getAntLauncherJarLocation(systemClassPath, classpathItems);
cmdLine.createArguments("-classpath", antLauncherJarLocation);
cmdLine.createArgument("org.apache.tools.ant.launch.Launcher");
cmdLine.createArguments("-lib", removeSaxonJars(classpathItems, isWindows));
}
if (progress == null) {
if (useLogger) {
cmdLine.createArguments("-logger", getLoggerClassName());
cmdLine.createArguments("-logfile", tempFileName);
} else {
cmdLine.createArguments("-listener", getLoggerClassName());
cmdLine.createArgument("-DXmlLogger.file=" + tempFileName);
}
} else {
// need to showProgress
// use proper default logger if loggerClassName was not specified by config
setupResolvedLoggerClassname();
cmdLine.createArguments("-logger", getLoggerClassName());
if (useLogger) {
// need to use AntProgressXmlLogger as a listener
cmdLine.createArguments("-listener", CLASSNAME_ANTPROGRESS_XML_LISTENER);
cmdLine.createArgument("-DXmlLogger.file=" + tempFileName);
} else {
cmdLine.createArguments("-listener", AntBuilder.DEFAULT_LOGGER);
cmdLine.createArgument("-DXmlLogger.file=" + tempFileName);
}
}
if (AntBuilder.shouldAddDashboardLoggerJarToCommandLine(showAntOutput, useLogger)) {
cmdLine.createArguments("-listener", CLASSNAME_DASHBOARD_LISTENER);
}
if ((progress != null)
|| AntBuilder.shouldAddDashboardLoggerJarToCommandLine(showAntOutput, useLogger)) {
// we need to add the custom logger jar {@link #LIBNAME_PROGRESS_LOGGER cruisecontrol-antprogresslogger.jar}
// to the ant VM class path as a lib
setupDefaultProgressLoggerLib();
// add -lib to progressLogger classes
cmdLine.createArguments("-lib", progressLoggerLib);
}
// -debug and -quiet only affect loggers, not listeners: when we use the loggerClassName as
// a listener, they will affect the default logger that writes to the console
if (useDebug) {
cmdLine.createArgument("-debug");
} else if (useQuiet) {
cmdLine.createArgument("-quiet");
}
if (keepGoing) {
cmdLine.createArgument("-keep-going");
}
for (final AntBuilder.Lib lib : libs) {
cmdLine.createArguments("-lib", lib.getSearchPath());
}
for (final AntBuilder.Listener listener : listeners) {
cmdLine.createArguments("-listener", listener.getClassName());
}
for (final Map.Entry property : buildProperties.entrySet()) {
final String value = (String) property.getValue();
if (!"".equals(value)) {
cmdLine.createArgument("-D" + property.getKey() + "=" + value);
}
}
for (final Property property : properties) {
cmdLine.createArgument("-D" + property.getName() + "=" + property.getValue());
}
if (propertyfile != null) {
cmdLine.createArguments("-propertyfile", propertyfile);
}
cmdLine.createArguments("-buildfile", buildFile);
cmdLine.setEnv(env);
final StringTokenizer targets = new StringTokenizer(target);
while (targets.hasMoreTokens()) {
cmdLine.createArgument(targets.nextToken());
}
return cmdLine;
}
/**
* @param path the classpath in which to search for the ant-launcher.jar
* @param isWindows true if running on Windows
* @return the path to ant-launcher*.jar taken from the given path
* @throws CruiseControlException if path to ant-launcher.jar could not be found.
*/
String getAntLauncherJarLocation(final String path, final boolean isWindows) throws CruiseControlException {
return getAntLauncherJarLocation(path, getClasspathItems(path, isWindows));
}
/**
* @param path the classpath as a single string, used here only for error message.
* @param classpathItems the classpath items to search for the ant-launcher.jar
* @return the path to ant-launcher*.jar taken from the given path
* @throws CruiseControlException if path to ant-launcher.jar could not be found.
*/
private String getAntLauncherJarLocation(final String path, final List<String> classpathItems)
throws CruiseControlException {
for (final String pathElement : classpathItems) {
if (pathElement.indexOf("ant-launcher") != -1 && pathElement.endsWith(".jar")) {
return pathElement;
}
}
throw new CruiseControlException("Couldn't find path to ant-launcher jar in this classpath: '" + path + "'");
}
/**
* @param path the classpath to split each element into a List
* @param isWindows true if running on Windows
* @return a List containing each element in the classpath
*/
List<String> getClasspathItems(final String path, final boolean isWindows) {
final List<String> ret = new ArrayList<String>();
final String separator = getSeparator(isWindows);
final StringTokenizer pathTokenizer = new StringTokenizer(path, separator);
while (pathTokenizer.hasMoreTokens()) {
final String pathElement = pathTokenizer.nextToken();
ret.add(pathElement);
}
return ret;
}
/**
* The Saxon jars cause the Ant junitreport task to fail.
*
* @param classpathItems a List containing items in a classpath
* @param isWindows true if running on Windows
* @return a String containing all the jars in the classpath minus the Saxon jars
*/
String removeSaxonJars(final List<String> classpathItems, final boolean isWindows) {
final StringBuilder path = new StringBuilder();
final String separator = getSeparator(isWindows);
for (final String pathElement : classpathItems) {
final File elementFile = new File(pathElement);
if (!elementFile.getName().startsWith("saxon")) {
if (path.length() > 0) {
path.append(separator);
}
path.append(pathElement);
}
}
return path.toString();
}
String removeSaxonJars(final String path, final boolean isWindows) {
return removeSaxonJars(getClasspathItems(path, isWindows), isWindows);
}
private String getSeparator(boolean isWindows) {
return isWindows ? ";" : ":";
}
void setupResolvedLoggerClassname() {
// use proper default logger if loggerClassName was not specified by config
if ((progress != null) && (!isLoggerClassNameSet)) {
if (useLogger) {
// use string to avoid dependence on ant classes
loggerClassName = CLASSNAME_ANTPROGRESS_XML_LOGGER;
} else {
// use string to avoid dependence on ant classes
loggerClassName = CLASSNAME_ANTPROGRESS_LOGGER;
}
} else {
if (progress != null) {
LOG.warn("Ant Progress support is enabled AND loggerClassname is set. "
+ "Be sure the loggerClassName: " + loggerClassName + " is compatible with"
+ " Ant Progress.");
}
}
LOG.debug("Using loggerClassName: " + loggerClassName);
}
private static final String MSG_RESOLUTION_PROGRESS_LOGGER_LIB
= "\n\tTo enable showAntOutput and/or showProgress, do one of the following: "
+ "\n\t1. Copy " + LIBNAME_PROGRESS_LOGGER + " to a directory, and set the full path (including filename) "
+ "\n\t\tto " + LIBNAME_PROGRESS_LOGGER + " in config.xml as the value of 'progressLoggerLib' for this "
+ "<ant> builder. "
+ "\n\t2. Set showAntOutput=false and/or showProgress=false for this <ant> builder."
+ "\n\t3. Copy " + LIBNAME_PROGRESS_LOGGER + " into your ant/lib directory."
+ "\n\tNote: Please report this issue, as not finding this library is most likely a boog.";
/**
* Finds the default location of the {@link AntScript#LIBNAME_PROGRESS_LOGGER cruisecontrol-antprogresslogger.jar}
* by first finding the location of the jar containing the {@link AntScript} class.
*
* @return the full path (including jar name) to the jar file
* ({@link #LIBNAME_PROGRESS_LOGGER cruisecontrol-antprogresslogger.jar})
* containing the AntProgressLogger/Listener classes.
* @throws ProgressLibLocatorException if the search class ({@link AntScript}) file can't be found,
* likely related to running under Java Webstart {@literal >=} 6, or simply if the jar can't be found
*/
public static String findDefaultProgressLoggerLib() throws ProgressLibLocatorException {
// find path (including filename) to progressLoggerLib jar
final String progressLoggerLib;
final File ccMain = UtilLocator.getClassSource(AntScript.class);
if (ccMain == null) {
throw new ProgressLibLocatorException(
"Could not determine -lib path for progressLoggerLib. (Java 6/Webstart issue?) "
+ MSG_RESOLUTION_PROGRESS_LOGGER_LIB);
} else {
final String pathToDirContainingCCMainJar;
if (ccMain.isDirectory()) {
pathToDirContainingCCMainJar = ccMain.getAbsolutePath();
} else {
pathToDirContainingCCMainJar = ccMain.getParentFile().getAbsolutePath();
}
final File expectedProgressLoggerJar = new File(pathToDirContainingCCMainJar, LIBNAME_PROGRESS_LOGGER);
if (expectedProgressLoggerJar.exists()) {
// Use the specific jar if that jar exists.
// This is a bit of a hack to load the progress logger jar into
// ant without loading other jars (such as, ant.jar for instance)
progressLoggerLib = expectedProgressLoggerJar.getAbsolutePath();
} else {
// Missing Progress Logger Lib is nasty to debug, so error out here if we can't find it for sure.
throw new ProgressLibLocatorException("The progressLoggerLib jar file does not exist where expected: "
+ expectedProgressLoggerJar.getAbsolutePath() + MSG_RESOLUTION_PROGRESS_LOGGER_LIB);
}
}
return progressLoggerLib;
}
void setupDefaultProgressLoggerLib() throws ProgressLibLocatorException {
if (progressLoggerLib == null) {
// Use a valid default for progressLoggerLib
progressLoggerLib = findDefaultProgressLoggerLib();
LOG.debug("Using default progressLoggerLib: " + progressLoggerLib);
}
}
public static final class ProgressLibLocatorException extends CruiseControlException {
private ProgressLibLocatorException(final String msg) {
super(msg);
}
}
/**
* Analyze the output of ant command, used to detect progress messages.
*/
public void consumeLine(final String line) {
if (progress != null && line != null
&& line.startsWith(MSG_PREFIX_ANT_PROGRESS)) {
progress.setValue(line.substring(MSG_PREFIX_ANT_PROGRESS.length()));
}
}
/**
* @param buildProperties The buildProperties to set.
*/
public void setBuildProperties(final Map<String, String> buildProperties) {
this.buildProperties = buildProperties;
}
/**
* @return Returns the loggerClassName.
*/
public String getLoggerClassName() {
return loggerClassName;
}
/**
* @param loggerClassName The loggerClassName to set.
*/
public void setLoggerClassName(String loggerClassName) {
this.loggerClassName = loggerClassName;
}
/**
* @param isLoggerClassNameSet The loggerClassName to set.
*/
public void setIsLoggerClassNameSet(boolean isLoggerClassNameSet) {
this.isLoggerClassNameSet = isLoggerClassNameSet;
}
/**
* @param showAntOutput if true use Dashboard AntOutputLogger (CLASSNAME_DASHBOARD_LISTENER) as listener IIF
* useLogger is also true
*/
public void setShowAntOutput(final boolean showAntOutput) {
this.showAntOutput = showAntOutput;
}
/**
* @param antScript The antScript to set.
*/
public void setAntScript(String antScript) {
this.antScript = antScript;
}
/**
* @param args The args to set.
*/
public void setArgs(final List<AntBuilder.JVMArg> args) {
this.args = args;
}
/**
* @param isWindows The isWindows to set.
*/
public void setWindows(boolean isWindows) {
this.isWindows = isWindows;
}
/**
* @param buildFile The buildFile to set.
*/
public void setBuildFile(String buildFile) {
this.buildFile = buildFile;
}
/**
* @param tempFileName The tempFileName to set.
*/
public void setTempFileName(String tempFileName) {
this.tempFileName = tempFileName;
}
/**
* @param useDebug The useDebug to set.
*/
public void setUseDebug(boolean useDebug) {
this.useDebug = useDebug;
}
/**
* @param useLogger The useLogger to set.
*/
public void setUseLogger(boolean useLogger) {
this.useLogger = useLogger;
}
/**
* @param useQuiet The useQuiet to set.
*/
public void setUseQuiet(boolean useQuiet) {
this.useQuiet = useQuiet;
}
public void setKeepGoing(boolean keepGoing) {
this.keepGoing = keepGoing;
}
/**
* @param useScript The useScript to set.
*/
public void setUseScript(boolean useScript) {
this.useScript = useScript;
}
/**
* @param systemClassPath The systemClassPath to set.
*/
public void setSystemClassPath(String systemClassPath) {
this.systemClassPath = systemClassPath;
}
/**
* @param properties The properties to set.
*/
public void setProperties(final List<Property> properties) {
this.properties = properties;
}
/**
* @param libs The set of library paths to use.
*/
public void setLibs(final List<AntBuilder.Lib> libs) {
this.libs = libs;
}
/**
* @param listeners The set of listener classes to use.
*/
public void setListeners(final List<AntBuilder.Listener> listeners) {
this.listeners = listeners;
}
/**
* @param target The target to set.
*/
public void setTarget(String target) {
this.target = target;
}
/**
* @return Returns the exitCode.
*/
public int getExitCode() {
return exitCode;
}
/**
* @param exitCode The exitCode to set.
*/
public void setExitCode(int exitCode) {
this.exitCode = exitCode;
}
/**
* @param propertyFile The properties file to set.
*/
public void setPropertyFile(String propertyFile) {
this.propertyfile = propertyFile;
}
/**
* @param progressLoggerLib The directory containing the AntProgressLogger/Listener classes.
*/
public void setProgressLoggerLib(final String progressLoggerLib) {
this.progressLoggerLib = progressLoggerLib;
}
/**
* @param progress The progress callback object to set.
*/
public void setProgress(final Progress progress) {
this.progress = progress;
}
/**
* @param env
* The environment variables of the ant script, or <code>null</code> if to
* inherit the environment of the current process.
*/
public void setAntEnv(final OSEnvironment env) {
this.env = env;
} // setAntEnv
}