package net.sourceforge.cruisecontrol.builders; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map; import net.sourceforge.cruisecontrol.Builder; import net.sourceforge.cruisecontrol.CruiseControlException; import net.sourceforge.cruisecontrol.Progress; import net.sourceforge.cruisecontrol.util.EmptyElementFilter; import net.sourceforge.cruisecontrol.util.Util; import net.sourceforge.cruisecontrol.util.ValidationHelper; import org.apache.log4j.Logger; import org.jdom.Element; import org.jdom.input.SAXBuilder; import org.xml.sax.SAXException; import org.xml.sax.XMLFilter; import org.xml.sax.helpers.XMLFilterImpl; public class PhingBuilder extends Builder { protected static final String DEFAULT_LOGGER = "phing.listener.XmlLogger"; private static final Logger LOG = Logger.getLogger(PhingBuilder.class); private String phingWorkingDir = null; private String buildFile = "build.xml"; private String target = ""; private String tempFileName = "log.xml"; private String phingScript = "phing"; private String phingHome; private boolean useLogger; private final List<Property> properties = new ArrayList<Property>(); private boolean useDebug = false; private boolean useQuiet = false; private String loggerClassName = DEFAULT_LOGGER; private File saveLogDir = null; private long timeout = ScriptRunner.NO_TIMEOUT; private boolean wasValidated = false; public void validate() throws CruiseControlException { super.validate(); ValidationHelper.assertIsSet(buildFile, "buildfile", this.getClass()); ValidationHelper.assertIsSet(target, "target", this.getClass()); ValidationHelper.assertFalse(useDebug && useQuiet, "'useDebug' and 'useQuiet' can't be used together"); if (!useLogger && (useDebug || useQuiet)) { LOG.warn("usedebug and usequiet are ignored if uselogger is not set to 'true'!"); } if (saveLogDir != null) { ValidationHelper.assertTrue(saveLogDir.isDirectory(), "'saveLogDir' must exist and be a directory"); } if (phingHome != null) { final File phingHomeFile = new File(phingHome); ValidationHelper.assertTrue(phingHomeFile.exists() && phingHomeFile.isDirectory(), "'phingHome' must exist and be a directory. Expected to find " + phingHomeFile.getAbsolutePath()); final File phingScriptInPhingHome = new File(findPhingScript(Util.isWindows())); ValidationHelper.assertTrue(phingScriptInPhingHome.exists() && phingScriptInPhingHome.isFile(), "'phingHome' must contain an ant execution script. Expected to find " + phingScriptInPhingHome.getAbsolutePath()); phingScript = phingScriptInPhingHome.getAbsolutePath(); } wasValidated = true; } /** * build and return the results via xml. debug status can be determined * from log4j category once we get all the logging in place. */ public Element build(final Map<String, String> buildProperties, final Progress progressIn) throws CruiseControlException { if (!wasValidated) { throw new IllegalStateException("This builder was never validated." + " The build method should not be getting called."); } validateBuildFileExists(); final Progress progress = getShowProgress() ? progressIn : null; // @todo To support progress, determine text pattern indicating progress messages in output, // see AntScript.consumeLine() as an example final PhingScript script = new PhingScript(); script.setBuildProperties(buildProperties); script.setProperties(properties); script.setUseLogger(useLogger); script.setPhingScript(phingScript); script.setBuildFile(buildFile); script.setTarget(target); script.setLoggerClassName(loggerClassName); script.setTempFileName(tempFileName); script.setUseDebug(useDebug); script.setUseQuiet(useQuiet); final File workingDir = phingWorkingDir != null ? new File(phingWorkingDir) : null; final boolean scriptCompleted = new ScriptRunner().runScript(workingDir, script, timeout, getBuildOutputConsumer(buildProperties.get(Builder.BUILD_PROP_PROJECTNAME), workingDir, null)); final File logFile = new File(phingWorkingDir, tempFileName); final Element buildLogElement; if (!scriptCompleted) { LOG.warn("Build timeout timer of " + timeout + " seconds has expired"); buildLogElement = new Element("build"); buildLogElement.setAttribute("error", "build timeout"); // although log file is most certainly empty, let's try to preserve it // somebody should really fix ant's XmlLogger if (logFile.exists()) { try { buildLogElement.setText(Util.readFileToString(logFile)); } catch (IOException likely) { } } } else { //read in log file as element, return it buildLogElement = getPhingLogAsElement(logFile); savePhingLog(logFile); logFile.delete(); } return buildLogElement; } public Element buildWithTarget(final Map<String, String> properties, final String buildTarget, final Progress progress) throws CruiseControlException { final String origTarget = target; try { target = buildTarget; return build(properties, progress); } finally { target = origTarget; } } void validateBuildFileExists() throws CruiseControlException { File build = new File(buildFile); if (!build.isAbsolute() && phingWorkingDir != null) { build = new File(phingWorkingDir, buildFile); } ValidationHelper.assertExists(build, "buildfile", this.getClass()); } /** * Set the location to which the ant log will be saved before Cruise * Control merges the file into its log. * * @param dir * the absolute path to the directory where the ant log will be * saved or relative path to where you started CruiseControl */ public void setSaveLogDir(String dir) { saveLogDir = null; if (dir != null && !dir.trim().equals("")) { saveLogDir = new File(dir.trim()); } } void savePhingLog(File logFile) { if (saveLogDir == null) { return; } try { final File newPhingLogFile = new File(saveLogDir, tempFileName); newPhingLogFile.createNewFile(); final FileInputStream in = new FileInputStream(logFile); try { final FileOutputStream out = new FileOutputStream(newPhingLogFile); try { byte[] buf = new byte[1024]; int len; while ((len = in.read(buf)) > 0) { out.write(buf, 0, len); } } finally { out.close(); } } finally { in.close(); } } catch (IOException ioe) { LOG.error(ioe); LOG.error("Unable to create file: " + new File(saveLogDir, tempFileName)); } } /** * Set the working directory where Phing will be invoked. This parameter gets * set in the XML file via the phingWorkingDir attribute. The directory can * be relative (to the cruisecontrol current working directory) or absolute. * * @param dir * the directory to make the current working directory. */ public void setPhingWorkingDir(String dir) { phingWorkingDir = dir; } /** * Sets the Phing script file to be invoked. * * This is a platform dependent script file. * * @param phingScript the name of the script file */ public void setPhingScript(String phingScript) { this.phingScript = phingScript; } /** * If set CC will use the platform specific script provided by Phing * * @param antHome the path to ANT_HOME */ public void setPhingHome(String antHome) { this.phingHome = antHome; } /** * If the phinghome attribute is set, then this method returns the correct shell script * to use for a specific environment. * @param isWindows true if running under windows * @return shell script to use for a specific environment. * @throws CruiseControlException if the phinghome attribute is not set */ protected String findPhingScript(boolean isWindows) throws CruiseControlException { if (phingHome == null) { throw new CruiseControlException("phinghome attribute not set."); } if (isWindows) { return phingHome + "\\bin\\phing.bat"; } else { return phingHome + "/bin/phing"; } } /** * @param tempFileName the name of the temporary file used to capture output. */ public void setTempFile(String tempFileName) { this.tempFileName = tempFileName; } /** * Set the Phing target(s) to invoke. * * @param target the target(s) name. */ public void setTarget(String target) { this.target = target; } /** * Sets the name of the build file that Phing will use. The Phing default is * build.xml, use this to override it. * * @param buildFile the name of the build file. */ public void setBuildFile(String buildFile) { this.buildFile = buildFile; } /** * Sets whether Phing will use the custom loggers. * @param useLogger if true, use custom loggers */ public void setUseLogger(boolean useLogger) { this.useLogger = useLogger; } public Property createProperty() { Property property = new Property(); properties.add(property); return property; } protected static Element getPhingLogAsElement(File file) throws CruiseControlException { if (!file.exists()) { throw new CruiseControlException("phing logfile " + file.getAbsolutePath() + " does not exist."); } else if (file.length() == 0) { throw new CruiseControlException("phing logfile " + file.getAbsolutePath() + " is empty. Your build probably failed. Check your CruiseControl logs."); } try { SAXBuilder builder = new SAXBuilder("org.apache.xerces.parsers.SAXParser"); // old Ant-versions contain a bug in the XmlLogger that outputs // an invalid PI containing the target "xml:stylesheet" // instead of "xml-stylesheet": fix this // FIXME - remove this, as it shouldn't affect Phing (though I don't know what this is // for yet.) XMLFilter piFilter = new XMLFilterImpl() { public void processingInstruction(String target, String data) throws SAXException { if (target.equals("xml:stylesheet")) { target = "xml-stylesheet"; } super.processingInstruction(target, data); } }; // get rid of empty <task>- and <message>-elements created by Ant's XmlLogger XMLFilter emptyTaskFilter = new EmptyElementFilter("task"); emptyTaskFilter.setParent(piFilter); XMLFilter emptyMessageFilter = new EmptyElementFilter("message"); emptyMessageFilter.setParent(emptyTaskFilter); builder.setXMLFilter(emptyMessageFilter); return builder.build(file).getRootElement(); } catch (Exception ee) { if (ee instanceof CruiseControlException) { throw (CruiseControlException) ee; } File saveFile = new File(file.getParentFile(), System.currentTimeMillis() + file.getName()); file.renameTo(saveFile); throw new CruiseControlException("Error reading : " + file.getAbsolutePath() + ". Saved as : " + saveFile.getAbsolutePath(), ee); } } public void setUseDebug(boolean debug) { useDebug = debug; } public void setUseQuiet(boolean quiet) { useQuiet = quiet; } public String getLoggerClassName() { return loggerClassName; } public void setLoggerClassName(String string) { loggerClassName = string; } /** * @param timeout The timeout to set. */ public void setTimeout(long timeout) { this.timeout = timeout; } }