/********************************************************************************
* 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.BufferedInputStream;
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.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* Source Control implementation for Darcs. Provides a means of executing the darcs changes command and parsing the xml
* output to determine if there have been any changes. The modifications are parsed and used for cruisecontrol build
* reports which allow the patch names associated with a build to be displayed. Currently the darcs xml-output does not
* display which files have changed.
*
* Large parts of this implementation were based on the {@link net.sourceforge.cruisecontrol.sourcecontrols.SVN} source
* control implementation.
*/
public class Darcs implements SourceControl {
private static final long serialVersionUID = 7976081409836256093L;
private static final Logger LOGGER = Logger.getLogger(Darcs.class);
private static final DateFormat DARCS_DATE_FORMAT_IN = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private String workingDir;
private String repositoryLocation;
private final SourceControlProperties properties = new SourceControlProperties();
public void setProperty(final String property) {
properties.assignPropertyName(property);
}
public void setPropertyOnDelete(final String propertyOnDelete) {
properties.assignPropertyOnDeleteName(propertyOnDelete);
}
public void setRepositoryLocation(final String repositoryLocation) {
this.repositoryLocation = repositoryLocation;
}
public void setWorkingDir(final String workingDir) {
this.workingDir = workingDir;
}
public Map<String, String> getProperties() {
return properties.getPropertiesAndReset();
}
public void validate() throws CruiseControlException {
ValidationHelper.assertTrue(repositoryLocation != null || workingDir != null,
"At least 'repositoryLocation'or 'workingDir' is a required attribute on the Darcs task ");
if (workingDir != null) {
File workingDirFile = new File(workingDir);
ValidationHelper.assertTrue(workingDirFile.exists() && workingDirFile.isDirectory(),
"'workingDir' must be an existing directory. Was " + workingDirFile.getAbsolutePath());
}
}
public List<Modification> getModifications(final Date lastBuild, final Date now) {
try {
final Commandline command = buildChangesCommand(lastBuild, now);
final List<Modification> modifications = execChangesCommand(command);
fillPropertiesIfNeeded(modifications);
return modifications;
} catch (Exception e) {
LOGGER.error("Failed to execute darcs changes command", e);
}
return Collections.emptyList();
}
Commandline buildChangesCommand(final Date lastBuild, final Date checkTime) throws CruiseControlException {
final Commandline command = new Commandline();
command.setExecutable("darcs");
if (workingDir != null) {
command.setWorkingDirectory(workingDir);
}
command.createArgument("changes");
command.createArgument("--xml-output");
command.createArgument("--matches");
command.createArgument("date \"" + DARCS_DATE_FORMAT_IN.format(lastBuild) + "/"
+ DARCS_DATE_FORMAT_IN.format(checkTime) + "\"");
LOGGER.debug("Executing command: " + command);
return command;
}
private List<Modification> execChangesCommand(final Commandline command)
throws InterruptedException, IOException, ParseException, JDOMException {
final Process p = command.execute();
final Thread stderr = logErrorStream(p);
final InputStream darcsStream = p.getInputStream();
final List<Modification> modifications = parseStream(darcsStream);
p.waitFor();
stderr.join();
IO.close(p);
return modifications;
}
private List<Modification> parseStream(final InputStream darcsStream)
throws ParseException, JDOMException, IOException {
InputStreamReader reader = new InputStreamReader(new BufferedInputStream(darcsStream), "UTF-8");
try {
return DarcsXmlParser.parse(reader);
} finally {
reader.close();
}
}
private Thread logErrorStream(final Process p) {
final Thread stderr = new Thread(StreamLogger.getWarnPumper(LOGGER, p.getErrorStream()));
stderr.start();
return stderr;
}
void fillPropertiesIfNeeded(final List<Modification> modifications) {
if (!modifications.isEmpty()) {
properties.modificationFound();
}
}
static final class DarcsXmlParser {
private static final DateFormat DARCS_DATE_FORMAT_OUT = new SimpleDateFormat("yyyyMMddHHmmss");
private DarcsXmlParser() { /* helper class, no instances */
}
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);
}
private static List<Modification> parseDOMTree(final Document document) throws ParseException {
final List<Modification> modifications = new ArrayList<Modification>();
final Element rootElement = document.getRootElement();
final List patches = rootElement.getChildren("patch");
if (patches != null) {
for (Iterator iterator = patches.iterator(); iterator.hasNext();) {
final Element patch = (Element) iterator.next();
modifications.add(parsePatch(patch));
}
}
return modifications;
}
private static Modification parsePatch(final Element patch) throws ParseException {
final Modification modification = new Modification("darcs");
modification.modifiedTime = DARCS_DATE_FORMAT_OUT.parse(patch.getAttributeValue("date"));
final String email = patch.getAttributeValue("author");
modification.userName = parseUser(email);
modification.emailAddress = email;
modification.comment = patch.getChildText("name");
return modification;
}
private static String parseUser(final String email) {
return email.substring(0, email.indexOf('@'));
}
}
}