/******************************************************************************** * CruiseControl, a Continuous Integration Toolkit * Copyright (c) 2007, 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.sourcecontrols; import net.sourceforge.cruisecontrol.CruiseControlException; import net.sourceforge.cruisecontrol.Modification; import net.sourceforge.cruisecontrol.SourceControl; import net.sourceforge.cruisecontrol.util.Commandline; import net.sourceforge.cruisecontrol.util.IO; import net.sourceforge.cruisecontrol.util.StreamLogger; import net.sourceforge.cruisecontrol.util.ValidationHelper; import org.apache.log4j.Logger; import org.jdom.Document; import org.jdom.Element; import org.jdom.JDOMException; import org.jdom.input.SAXBuilder; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.text.DateFormat; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; import java.util.StringTokenizer; import java.util.TimeZone; /** * This class implements the SourceControl methods for a Store repository, which * is the version control system used by Cincom Smalltalk Visualworks. * * @see <a href="http://smalltalk.cincom.com/">smalltalk.cincom.com</a> * @author <a href="rcoulman@gmail.com">Randy Coulman</a> */ public class Store implements SourceControl { private static final Logger LOG = Logger.getLogger(Store.class); /** Date format expected by Store */ private static final String STORE_DATE_FORMAT = "MM/dd/yyyy HH:mm:ss.SSS"; private final SourceControlProperties properties = new SourceControlProperties(); /** Configuration parameters */ private String workingDirectory; private String script; private String profile; private List<String> packages; private String versionRegex; private String minimumBlessingLevel; private String parcelBuilderFile; public Map<String, String> getProperties() { return properties.getPropertiesAndReset(); } public void setProperty(String property) { properties.assignPropertyName(property); } /** * Sets the working directory to use when interacting with Store. * * @param directory String indicating the directory to use as the * working directory */ public void setWorkingDirectory(String directory) { this.workingDirectory = directory; } /** * Sets the script to use to make calls to Store. * * This script should start a VisualWorks image with the CruiseControl * package loaded and pass on the rest of the command-line arguments * supplied by this plugin. * * @param script String indicating the executable script to * use when making calls to Store. */ public void setScript(String script) { this.script = script; } /** * Sets the name of the Store profile to check for modifications. * * @param profile String indicating the name of the Store profile to * connect to when checking for modifications */ public void setProfile(String profile) { this.profile = profile; } /** * Sets the list of Store packages to be checked. * * @param packageNames a comma-separated list of package names */ public void setPackages(final String packageNames) { if (packageNames != null) { final StringTokenizer st = new StringTokenizer(packageNames, ","); this.packages = new ArrayList<String>(); while (st.hasMoreTokens()) { this.packages.add(st.nextToken()); } } } /** * Sets a regex to use to select versions of interest. * * @param regex String containing a regular expression that * matches versions of interest */ public void setVersionRegex(String regex) { this.versionRegex = regex; } /** * Sets a minimum blessing level to select versions of interest. * * @param blessing String containing the minimum blessing level * that package versions must have to be included */ public void setMinimumBlessingLevel(String blessing) { this.minimumBlessingLevel = blessing; } /** * Sets the name of a file to store the list of head package versions. * * @param filename String containing the filename used to store input * for ParcelBuilder to use to deploy parcels */ public void setParcelBuilderFile(String filename) { this.parcelBuilderFile = filename; } /** * This method validates that at least the repository location or the local * working copy location has been specified. * * @throws CruiseControlException Thrown when the repository location and * the local working copy location are both * null */ public void validate() throws CruiseControlException { ValidationHelper.assertTrue(workingDirectory != null, "'workingDirectory is a required attribute on the Store task"); if (workingDirectory != null) { File directory = new File(workingDirectory); ValidationHelper.assertTrue(directory.exists() && directory.isDirectory(), "'workingDirectory' must be an existing directory. Was " + directory.getAbsolutePath()); } ValidationHelper.assertTrue(script != null, "'script' is a required attribute on the Store task"); if (script != null) { File scriptFile = new File(script); ValidationHelper.assertTrue(scriptFile.exists(), "'script' must be an existing file. Was " + scriptFile.getAbsolutePath()); } ValidationHelper.assertTrue(profile != null, "'profile' is a required attribute on the Store task"); ValidationHelper.assertTrue(packages != null, "'packages' is a required attribute on the Store task"); ValidationHelper.assertTrue(packages.size() > 0, "'packages' must specify at least one package"); } /** * Returns a list of modifications detailing all the changes between * the last build and the latest revision in the repository. * @return the list of modifications, or an empty list if we failed * to retrieve the changes. */ public List<Modification> getModifications(final Date lastBuild, final Date now) { List<Modification> modifications = new ArrayList<Modification>(); final Commandline command; try { command = buildCommand(lastBuild, now); } catch (CruiseControlException e) { LOG.error("Error building history command", e); return modifications; } try { modifications = execCommand(command); } catch (Exception e) { LOG.error("Error executing svn log command " + command, e); } fillPropertiesIfNeeded(modifications); return modifications; } /** * Generates the command line for the store log command. * * For example: * * 'storeScript -profile local -packages PackageA "Package B" -lastBuild {lastbuildtime} -now {currentTime} -check' * @param lastBuild last build date * @param checkTime time source control check was run * @return command object to execute * @throws CruiseControlException if something breaks */ Commandline buildCommand(final Date lastBuild, final Date checkTime) throws CruiseControlException { final Commandline command = new Commandline(); command.setWorkingDirectory(workingDirectory); command.setExecutable(script); command.createArguments("-profile", profile); command.createArgument("-packages"); for (final String aPackage : packages) { command.createArgument(aPackage); } if (versionRegex != null) { command.createArguments("-versionRegex", versionRegex); } if (minimumBlessingLevel != null) { command.createArguments("-blessedAtLeast", minimumBlessingLevel); } command.createArguments("-lastBuild", formatDate(lastBuild)); command.createArguments("-now", formatDate(checkTime)); if (parcelBuilderFile != null) { command.createArguments("-parcelBuilderFile", parcelBuilderFile); } command.createArgument("-check"); LOG.debug("Executing command: " + command); return command; } static String formatDate(Date date) { return getDateFormatter().format(date); } private List<Modification> execCommand(final Commandline command) throws InterruptedException, IOException, ParseException, JDOMException { final Process p = command.execute(); final Thread stderr = logErrorStream(p); final InputStream storeStream = p.getInputStream(); final List<Modification> modifications = parseStream(storeStream); p.waitFor(); stderr.join(); IO.close(p); return modifications; } private static Thread logErrorStream(Process p) { Thread stderr = new Thread(StreamLogger.getWarnPumper(LOG, p.getErrorStream())); stderr.start(); return stderr; } private List<Modification> parseStream(final InputStream storeStream) throws JDOMException, IOException, ParseException { final InputStreamReader reader = new InputStreamReader(storeStream, "UTF-8"); try { return StoreLogXMLParser.parse(reader); } finally { reader.close(); } } void fillPropertiesIfNeeded(List modifications) { if (!modifications.isEmpty()) { properties.modificationFound(); } } public static DateFormat getDateFormatter() { DateFormat f = new SimpleDateFormat(STORE_DATE_FORMAT); f.setTimeZone(TimeZone.getTimeZone("GMT")); return f; } static final class StoreLogXMLParser { private StoreLogXMLParser() { } static List<Modification> parse(final Reader reader) throws ParseException, JDOMException, IOException { final SAXBuilder builder = new SAXBuilder(false); final Document document = builder.build(reader); return parseDOMTree(document); } static List<Modification> parseDOMTree(final Document document) throws ParseException { final List<Modification> modifications = new ArrayList<Modification>(); final Element rootElement = document.getRootElement(); final List packageEntries = rootElement.getChildren("package"); for (final Object packageEntry1 : packageEntries) { Element packageEntry = (Element) packageEntry1; final List<Modification> modificationsOfRevision = parsePackageEntry(packageEntry); modifications.addAll(modificationsOfRevision); } return modifications; } static List<Modification> parsePackageEntry(final Element packageEntry) throws ParseException { final List<Modification> modifications = new ArrayList<Modification>(); final List blessings = packageEntry.getChildren("blessing"); for (final Object blessing1 : blessings) { Element blessing = (Element) blessing1; Modification modification = new Modification("store"); modification.modifiedTime = convertDate(blessing.getAttributeValue("timestamp")); modification.userName = blessing.getAttributeValue("user"); modification.comment = blessing.getText(); modification.revision = packageEntry.getAttributeValue("version"); Modification.ModifiedFile modfile = modification.createModifiedFile(packageEntry.getAttributeValue("name"), null); modfile.action = packageEntry.getAttributeValue("action"); modfile.revision = modification.revision; modifications.add(modification); } return modifications; } /** * Converts the specified Store date string into a Date. * @param date with format "MM/dd/yyyy HH:mm:ss.SSS" * @return converted date * @throws ParseException if specified date doesn't match the expected format */ static Date convertDate(String date) throws ParseException { return getDateFormatter().parse(date); } } }