/********************************************************************************
* CruiseControl, a Continuous Integration Toolkit
* Copyright (c) 2001, 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 java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.Locale;
import net.sourceforge.cruisecontrol.CruiseControlException;
import net.sourceforge.cruisecontrol.SourceControl;
import net.sourceforge.cruisecontrol.Modification;
import net.sourceforge.cruisecontrol.gendoc.annotations.Default;
import net.sourceforge.cruisecontrol.gendoc.annotations.Description;
import net.sourceforge.cruisecontrol.gendoc.annotations.DescriptionFile;
import net.sourceforge.cruisecontrol.gendoc.annotations.Optional;
import net.sourceforge.cruisecontrol.gendoc.annotations.Required;
import net.sourceforge.cruisecontrol.util.DiscardConsumer;
import net.sourceforge.cruisecontrol.util.StreamPumper;
import net.sourceforge.cruisecontrol.util.ValidationHelper;
import net.sourceforge.cruisecontrol.util.IO;
import net.sourceforge.cruisecontrol.util.StreamLogger;
import org.apache.log4j.Logger;
/**
* This class implements the SourceControlElement methods for a Clear Case
* repository.
*
* @author Thomas Leseney
* @author <a href="mailto:jcyip@thoughtworks.com">Jason Yip</a>
* @author Eric Lefevre
* @author Ralf Krakowski
*/
@DescriptionFile
public class ClearCase implements SourceControl {
private static final int DEFAULT = 0;
private static final int DISABLED = 1;
private static final int ENABLED = 2;
private static final Logger LOG = Logger.getLogger(ClearCase.class);
private final SourceControlProperties properties = new SourceControlProperties();
/**
* The path of the clear case view
*/
private String viewPath;
/**
* The branch to check for modifications
*/
private String branch;
private int recursive = DEFAULT; // default is true
private int all = DEFAULT; // default is false
/**
* Date format required by commands passed to Clear Case
* As per: http://jira.public.thoughtworks.org/browse/CC-818
* The cleartool command line always take date in US format and does not mind of the
* language set in the operating System.
*/
private final SimpleDateFormat inDateFormatter =
new SimpleDateFormat("dd-MMMM-yyyy.HH:mm:ss", Locale.US);
/**
* Date format returned in the output of Clear Case commands.
*/
private final SimpleDateFormat outDateFormatter =
new SimpleDateFormat("yyyyMMdd.HHmmss");
/**
* Unlikely combination of characters to separate fields in a ClearCase query
*/
static final String DELIMITER = "#~#";
/**
* Even more unlikely combination of characters to indicate end of one line in query.
* Carriage return (\n) can be used in comments and so is not available to us.
*/
static final String END_OF_STRING_DELIMITER = "@#@#@#@#@#@#@#@#@#@#@#@";
/**
* Sets the local working copy to use when making queries.
*
* @param path the local working copy to use when making queries.
*/
@Description("Local working copy to use when making queries")
@Required
public void setViewpath(final String path) {
//_viewPath = getAntTask().getProject().resolveFile(path).getAbsolutePath();
viewPath = new File(path).getAbsolutePath();
}
/**
* Sets the branch that we're concerned about checking files into.
*
* @param branch the branch that we're concerned about checking files into.
*/
@Description("The ClearCase branch")
@Required
public void setBranch(final String branch) {
this.branch = branch;
}
/**
* Set whether to check against sub-folders in the view path
* @param recursive whether to check against sub-folders in the view path
*/
@Description("Whether to check sub-folders in the viewpath.")
@Optional
@Default("true")
public void setRecursive(final boolean recursive) {
this.recursive = recursive ? ENABLED : DISABLED;
}
/**
* Set when checking the entire view path.
* <p>
* When checking the entire view path this option invokes 'lshistory -all'
* instead of 'lshistory -recursive', which is much faster.
* <p>
* This option is mutually exclusive with the recursive property.
* <p>
* Note that 'all' does not use your view's config-spec rules. It behaves
* like having a single line config-spec that selects just ELEMENT * /{@code <branch>}/LATEST
* (i.e. 'lshistory -all' results that contain @@ are discarded). This differs from
* 'recurse', which only shows items selected by your current view.
* @param all true when checking the entire view path
*/
@Description("Set when checking the entire view path. When checking the entire view "
+ "path this option invokes 'lshistory -all' instead of 'lshistory -recursive', "
+ "which is much faster.<br/><br/> This option is mutually exclusive with the "
+ "recursive property.<br/><br/> Note that 'all' does not use your view's "
+ "config-spec rules. It behaves like having a single line config-spec that "
+ "selects just ELEMENT * /<branch>/LATEST (i.e. 'lshistory -all' results "
+ "that contain @@ are discarded). This differs from 'recurse', which only "
+ "shows items selected by your current view.")
@Optional
@Default("false")
public void setAll(final boolean all) {
this.all = all ? ENABLED : DISABLED;
if (this.recursive == DEFAULT && all) {
this.recursive = DISABLED;
}
}
@Description("Will set this property if a modification has occurred. For use in "
+ "conditionally controlling the build later.")
@Optional
public void setProperty(final String property) {
properties.assignPropertyName(property);
}
public Map<String, String> getProperties() {
return properties.getPropertiesAndReset();
}
public void validate() throws CruiseControlException {
ValidationHelper.assertIsSet(viewPath, "viewpath", this.getClass());
if (recursive == ENABLED && all == ENABLED) {
ValidationHelper.fail("'recursive' and 'all' are mutually exclusive attributes for ClearCase");
}
}
/**
* Returns an {@link java.util.List List} of {@link ClearCaseModification}
* detailing all the changes between now and the last build.
*
* @param lastBuild the last build time
* @param now time now, or time to check, NOT USED
* @return the list of modifications, an empty (not null) list if no
* modifications.
*/
public List<Modification> getModifications(final Date lastBuild, final Date now) {
final String lastBuildDate = inDateFormatter.format(lastBuild);
properties.put("clearcaselastbuild", lastBuildDate);
properties.put("clearcasenow", inDateFormatter.format(now));
/*
* let's try a different clearcase command--this one just takes
* waaaaaaaay too long.
* String command = "cleartool find " + _viewPath +
* " -type f -exec \"cleartool lshistory" +
* " -since " + lastBuildDate;
* if(_branch != null)
* command += " -branch " + _branch;
* command += " -nco" + // exclude check out events
* " -fmt \\\" %u;%Nd;%n;%o \\n \\\" \\\"%CLEARCASE_XPN%\\\" \"";
*/
String command = "cleartool lshistory";
if (branch != null) {
command += " -branch " + branch;
}
if (recursive == DEFAULT || recursive == ENABLED) {
command += " -r";
} else if (all == ENABLED) {
command += " -all";
}
command += " -nco -since " + lastBuildDate;
command += " -fmt %u"
+ DELIMITER
+ "%Nd"
+ DELIMITER
+ "%En"
+ DELIMITER
+ "%Vn"
+ DELIMITER
+ "%o"
+ DELIMITER
+ "!%l"
+ DELIMITER
+ "!%a"
+ DELIMITER
+ "%Nc"
+ END_OF_STRING_DELIMITER
+ "\\n";
final File root = new File(viewPath);
LOG.info("ClearCase: getting modifications for " + viewPath);
LOG.debug("Command to execute : " + command);
List<Modification> modifications = null;
try {
Process p = Runtime.getRuntime().exec(command, null, root);
p.getOutputStream().close();
Thread stderr = logErrorStream(p);
InputStream input = p.getInputStream();
modifications = parseStream(input);
getRidOfLeftoverData(input);
p.waitFor();
stderr.join();
IO.close(p);
} catch (Exception e) {
LOG.error("Error in executing the Clear Case command : ", e);
}
if (modifications == null) {
modifications = new ArrayList<Modification>();
}
return modifications;
}
private Thread logErrorStream(final Process process) {
final Thread stderr = new Thread(StreamLogger.getWarnPumper(LOG, process));
stderr.start();
return stderr;
}
private void getRidOfLeftoverData(final InputStream stream) {
new StreamPumper(stream, new DiscardConsumer()).run();
}
/**
* Parses the input stream to construct the modifications list.
* Package-private to make it available to the unit test.
*
* @param input the stream to parse
* @return a list of modification elements
* @throws IOException if something breaks
*/
List<Modification> parseStream(final InputStream input) throws IOException {
final ArrayList<Modification> modifications = new ArrayList<Modification>();
final BufferedReader reader = new BufferedReader(new InputStreamReader(input));
final String ls = System.getProperty("line.separator");
String line;
String lines = "";
while ((line = reader.readLine()) != null) {
if (!lines.equals("")) {
lines += ls;
}
lines += line;
ClearCaseModification mod = null;
if (lines.indexOf(END_OF_STRING_DELIMITER) > -1) {
mod = parseEntry(lines.substring(0, lines.indexOf(END_OF_STRING_DELIMITER)));
lines = "";
}
if (mod != null) {
modifications.add(mod);
}
}
return modifications;
}
/**
* Parses a single line from the reader. Each line contains a single revision
* with the format : <br>
* username#~#date_of_revision#~#element_name#~#operation_type#~#comments <br>
*
* @param line the line to parse
* @return a modification element corresponding to the given line
*/
private ClearCaseModification parseEntry(final String line) {
LOG.debug("parsing entry: " + line);
final String[] tokens = tokeniseEntry(line);
if (tokens == null) {
return null;
}
final String username = tokens[0].trim();
final String timeStamp = tokens[1].trim();
final String elementName = tokens[2].trim();
final String version = tokens[3].trim();
final String operationType = tokens[4].trim();
final String labelList = tokens[5].substring(1).trim();
final List<String> labels = extractLabelsList(labelList);
final String attributeList = tokens[6].substring(1).trim();
final Hashtable<String, String> attributes = extractAttributesMap(attributeList);
final String comment = tokens[7].trim();
// A branch event shouldn't trigger a build
if (operationType.equals("mkbranch") || operationType.equals("rmbranch")) {
return null;
}
// Element names that contain @@ are discarded (see setAll(boolean))
if (elementName.indexOf("@@") >= 0) {
return null;
}
final ClearCaseModification mod = new ClearCaseModification();
mod.userName = username;
mod.revision = version;
final String folderName, fileName;
final int sep = elementName.lastIndexOf(File.separator);
if (sep > -1) {
folderName = elementName.substring(0, sep);
fileName = elementName.substring(sep + 1);
} else {
folderName = null;
fileName = elementName;
}
final ClearCaseModification.ModifiedFile modfile = mod.createModifiedFile(fileName, folderName);
try {
mod.modifiedTime = outDateFormatter.parse(timeStamp);
} catch (ParseException e) {
mod.modifiedTime = null;
}
modfile.action = operationType;
modfile.revision = version;
mod.type = "clearcase";
mod.labels = labels;
mod.attributes = attributes;
mod.comment = comment;
properties.modificationFound();
// TODO: check if operation type is a delete
return mod;
}
private String[] tokeniseEntry(final String line) {
final int maxTokens = 8;
final int minTokens = maxTokens - 1; // comment may be absent.
final String[] tokens = new String[maxTokens];
Arrays.fill(tokens, "");
int tokenIndex = 0;
for (int oldIndex = 0, index = line.indexOf(DELIMITER, 0); true;
oldIndex = index + DELIMITER.length(), index = line.indexOf(DELIMITER, oldIndex), tokenIndex++) {
if (tokenIndex > maxTokens) {
LOG.debug("Too many tokens; skipping entry");
return null;
}
if (index == -1) {
tokens[tokenIndex] = line.substring(oldIndex);
break;
} else {
tokens[tokenIndex] = line.substring(oldIndex, index);
}
}
if (tokenIndex < minTokens) {
LOG.debug("Not enough tokens; skipping entry");
return null;
}
return tokens;
}
/**
* @param attributeList attribute list
* @return parsed list
*/
private Hashtable<String, String> extractAttributesMap(final String attributeList) {
Hashtable<String, String> attributes = null;
if (attributeList.length() > 0) {
attributes = new Hashtable<String, String>();
StringTokenizer attrST = new StringTokenizer(attributeList, "(), ");
while (attrST.hasMoreTokens()) {
String attr = attrST.nextToken();
int idx = attr.indexOf('=');
if (idx > 0) {
String attrName = attr.substring(0, idx);
String attrValue = attr.substring(idx + 1);
if (attrValue.startsWith("\"")) {
attrValue = attrValue.substring(1, attrValue.length() - 1);
}
attributes.put(attrName, attrValue);
}
}
}
return attributes;
}
/**
* @param labelList label list
* @return parsed list
*/
private List<String> extractLabelsList(final String labelList) {
List<String> labels = null;
if (labelList.length() > 0) {
labels = new ArrayList<String>();
final StringTokenizer labelST = new StringTokenizer(labelList, "(), ");
while (labelST.hasMoreTokens()) {
labels.add(labelST.nextToken().trim());
}
}
return labels;
}
}