/********************************************************************************
* CruiseControl, a Continuous Integration Toolkit
* Copyright (c) 2006, 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.labelincrementers;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
import java.util.StringTokenizer;
import net.sourceforge.cruisecontrol.CruiseControlException;
import net.sourceforge.cruisecontrol.LabelIncrementer;
import net.sourceforge.cruisecontrol.util.Commandline;
import net.sourceforge.cruisecontrol.util.ValidationHelper;
import net.sourceforge.cruisecontrol.util.IO;
import net.sourceforge.cruisecontrol.util.StreamLogger;
import org.apache.log4j.Logger;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.Delete;
import org.apache.tools.ant.types.FileSet;
import org.apache.tools.ant.types.PatternSet.NameEntry;
import org.jdom.Element;
/**
* This class uses the most current changelist of the user in Perforce as the
* label for the builds. It can also sync the Perforce managed files to that
* changelist number, as well as clean out the existing managed files.
*
* @author <a href="mailto:groboclown@users.sourceforge.net">Matt Albrecht</a>
*/
public class P4ChangelistLabelIncrementer implements LabelIncrementer {
private static final Logger LOG =
Logger.getLogger(P4ChangelistLabelIncrementer.class);
private static final String CHANGELIST_PREFIX = "@";
private static final String REVISION_PREFIX = "#";
private static final String RECURSE_U = "/...";
private static final String RECURSE_W = "\\...";
private String p4Port;
private String p4Client;
private String p4User;
private String p4View;
private String p4Passwd;
private boolean clean = false;
private boolean delete = false;
private boolean sync = true;
private int baseChangelist = -1;
/**
* Retrieves the current changelist, or, if given, the specified changelist,
* and also performs any necessary actions the user requested.
*
* @param oldLabel Label from previous successful build.
* @return Label to use for most recent successful build.
*/
public String incrementLabel(String oldLabel, Element buildLog) {
String label = null;
try {
validate();
// Perform conditional actions.
// Since the settings might change or be executed in any order,
// we perform the checks on which actions to run here.
boolean delTree = delete;
boolean cleanP4 = delTree || clean;
boolean syncP4 = cleanP4 || sync;
if (cleanP4) {
LOG.info("Cleaning Perforce clientspec " + p4Client);
syncTo(REVISION_PREFIX + 0);
}
if (delTree) {
deleteView();
}
label = getDefaultLabel();
if (syncP4) {
syncTo(CHANGELIST_PREFIX + label);
}
} catch (CruiseControlException cce) {
LOG.warn("Couldn't run expected tasks", cce);
}
return label;
}
public boolean isPreBuildIncrementer() {
// This only has use when used as a pre-build incrementer
return true;
}
/**
* Verify that the label specified -- the previous label -- is a valid label.
* In this case any label is valid because the next label will not be based on
* previous label but on information from Perforce.
*/
public boolean isValidLabel(String label) {
return true;
}
/**
* The instance must be fully initialized before calling this method.
* @throws IllegalStateException if the instance is not properly initialized
*/
public String getDefaultLabel() {
if (baseChangelist > 0) {
return Integer.toString(baseChangelist);
}
// else
try {
validate();
return getCurrentChangelist();
} catch (CruiseControlException cce) {
cce.printStackTrace();
LOG.fatal("Problem accessing Perforce changelist", cce);
throw new IllegalStateException(
"Problem accessing Perforce changelist");
}
}
// User settings
/**
* Set the changelist number that you want to build at. If this isn't
* set, then the class will get the most current submitted changelist
* number. Note that setting this will cause the build to ALWAYS build
* at this changelist number.
*
* @param syncChange the changelist number to perform the sync to.
*/
public void setChangelist(int syncChange) {
baseChangelist = syncChange;
}
public void setPort(String p4Port) {
this.p4Port = p4Port;
}
public void setClient(String p4Client) {
this.p4Client = p4Client;
}
public void setUser(String p4User) {
this.p4User = p4User;
}
public void setView(String p4View) {
this.p4View = p4View;
}
public void setPasswd(String p4Passwd) {
this.p4Passwd = p4Passwd;
}
/**
* Disables the label incrementer from synchronizing Perforce to the
* view.
*
* @param b if true, Disables the label incrementer from synchronizing Perforce to the
* view.
*/
public void setNoSync(boolean b) {
this.sync = !b;
}
/**
* Perform a "p4 sync -f [view]#0" before syncing anew. This will force
* the sync to happen.
*
* @param b if true, perform a "p4 sync -f [view]#0" before syncing anew
*/
public void setClean(boolean b) {
this.clean = b;
}
/**
* Perform a recursive delete of the clientspec view. This
* will force a clean {@literal &} sync. Note that this can potentially
* be very destructive, so use with the utmost caution.
*
* @param b if true, force clean {@literal &} sync
*/
public void setDelete(boolean b) {
this.delete = b;
}
public void validate() throws CruiseControlException {
ValidationHelper.assertIsSet(p4View, "view", this.getClass());
ValidationHelper.assertNotEmpty(p4View, "view", this.getClass());
ValidationHelper.assertNotEmpty(p4Client, "client", this.getClass());
ValidationHelper.assertNotEmpty(p4Port, "port", this.getClass());
ValidationHelper.assertNotEmpty(p4User, "user", this.getClass());
ValidationHelper.assertNotEmpty(p4Passwd, "passwd", this.getClass());
}
protected String getCurrentChangelist()
throws CruiseControlException {
Commandline cmd = buildBaseP4Command();
cmd.createArgument("changes");
cmd.createArgument("-m1");
cmd.createArgument("-ssubmitted");
ParseChangelistNumbers pcn = new ParseChangelistNumbers();
runP4Cmd(cmd, pcn);
String[] changes = pcn.getChangelistNumbers();
if (changes != null && changes.length == 1) {
return changes[0];
} else {
throw new CruiseControlException(
"Could not discover the changelist");
}
}
protected void syncTo(String viewArg) throws CruiseControlException {
Commandline cmd = buildBaseP4Command();
cmd.createArguments("sync", p4View + viewArg);
runP4Cmd(cmd, new P4CmdParserAdapter());
}
protected void deleteView() throws CruiseControlException {
// despite what people tell you, deleting correctly in Java is
// hard. So, let Ant do our dirty work for us.
try {
Project p = createProject();
FileSet fs = getWhereView(p);
Delete d = createDelete(p);
d.setProject(p);
d.setVerbose(true);
d.addFileset(fs);
d.execute();
} catch (BuildException be) {
throw new CruiseControlException(be.getMessage(), be);
}
}
/**
* If the view mapping contains a reference to a single file,
*
* @param p project
* @return the collection of recursive directories inside the Perforce
* view.
* @throws CruiseControlException if something breaks
*/
protected FileSet getWhereView(final Project p) throws CruiseControlException {
String view = p4View;
if (view == null) {
view = "//...";
}
if (!view.endsWith(RECURSE_U) && !view.endsWith(RECURSE_W)) {
// we'll only care about the recursive view. Anything else
// should be handled by the sync view#0
LOG.debug("view [" + view + "] isn't recursive.");
return null;
}
final Commandline cmd = buildBaseP4Command();
cmd.createArguments("where", view);
final ParseOutputParam pop = new ParseOutputParam("");
runP4Cmd(cmd, pop);
final String[] values = pop.getValues();
if (values == null || values.length <= 0) {
LOG.debug("Didn't find any files for view");
return null;
}
final FileSet fs = createFileSet(p);
// on windows, this is considered higher than the drive letter.
fs.setDir(new File("/"));
int count = 0;
for (final String s : values) {
// first token: the depot name
// second token: the client name
// third token+: the local file system name
// like above, we only care about the recursive view. If the
// line doesn't end in /... or \... (even if it's a %%1), we ignore
// it. This makes our life so much simpler when dealing with
// spaces.
//LOG.debug("Parsing view line " + i + " [" + s + "]");
if (!s.endsWith(RECURSE_U) && !s.endsWith(RECURSE_W)) {
continue;
}
final String[] tokens = new String[3];
int pos = 0;
for (int j = 0; j < 3; ++j) {
final StringBuffer sb = new StringBuffer();
boolean neot = true;
while (neot) {
if (pos >= s.length()) {
break;
}
final int q1 = s.indexOf('\'', pos);
final int q2 = s.indexOf('"', pos);
final int sp = s.indexOf(' ', pos);
if (q1 >= 0 && (q1 < q2 || q2 < 0) && (q1 < sp || sp < 0)) {
sb.append(s.substring(pos, q1));
pos = q1 + 1;
} else if (q2 >= 0 && (q2 < q1 || q1 < 0) && (q2 < sp || sp < 0)) {
sb.append(s.substring(pos, q2));
pos = q2 + 1;
} else if (sp >= 0) {
// check if we're at the end of the token
final String sub = s.substring(pos, sp);
pos = sp + 1;
sb.append(sub);
if (sub.endsWith(RECURSE_U) || sub.endsWith(RECURSE_W)) {
neot = false;
} else {
// keep the space - it's inside the token
sb.append(' ');
}
} else {
sb.append(s.substring(pos));
neot = false;
}
}
tokens[j] = new String(sb).trim();
}
if (tokens[0] != null && tokens[1] != null && tokens[2] != null
&& (tokens[2].endsWith(RECURSE_U)
|| tokens[2].endsWith(RECURSE_W))) {
// convert the P4 recurse expression with the Ant
// recurse expression
final String f = tokens[2].substring(0,
tokens[2].length() - RECURSE_W.length())
+ File.separator + "**";
// a - in front of the depot name means to exclude this path
if (tokens[0].startsWith("-//")) {
final NameEntry ne = fs.createExclude();
ne.setName(f);
} else {
final NameEntry ne = fs.createInclude();
ne.setName(f);
}
++count;
}
}
if (count > 0) {
return fs;
} else {
LOG.debug("no files in view to delete");
return null;
}
}
protected Project createProject() {
final Project p = new Project();
p.init();
return p;
}
protected Delete createDelete(final Project p) throws CruiseControlException {
Object o = p.createTask("delete");
if (o == null || !(o instanceof Delete)) {
// Backup code just in case we didn't work right.
// If we can guarantee the above operation works all the time,
// then this log note should be replaced with an exception.
LOG.info("Could not find <delete> task in Ant. Defaulting to basic constructor.");
final Delete d = new Delete();
d.setProject(p);
o = d;
}
return (Delete) o;
}
protected FileSet createFileSet(final Project p) throws CruiseControlException {
Object o = p.createDataType("fileset");
if (o == null || !(o instanceof FileSet)) {
// Backup code just in case we didn't work right.
// If we can guarantee the above operation works all the time,
// then this log note should be replaced with an exception.
LOG.info("Could not find <fileset> type in Ant. Defaulting to basic constructor.");
final FileSet fs = new FileSet();
fs.setProject(p);
o = fs;
}
return (FileSet) o;
}
protected Commandline buildBaseP4Command() {
final Commandline commandLine = new Commandline();
commandLine.setExecutable("p4");
commandLine.createArgument("-s");
if (p4Client != null) {
commandLine.createArguments("-c", p4Client);
}
if (p4Port != null) {
commandLine.createArguments("-p", p4Port);
}
if (p4User != null) {
commandLine.createArguments("-u", p4User);
}
if (p4Passwd != null) {
commandLine.createArguments("-P", p4Passwd);
}
return commandLine;
}
protected void runP4Cmd(final Commandline cmd, final P4CmdParser parser)
throws CruiseControlException {
try {
final Process p = cmd.execute();
try {
Thread stderr = new Thread(StreamLogger.getWarnPumper(LOG, p));
stderr.start();
InputStream p4Stream = p.getInputStream();
parseStream(p4Stream, parser);
stderr.join();
} finally {
p.waitFor();
IO.close(p);
}
} catch (IOException e) {
throw new CruiseControlException("Problem trying to execute command line process", e);
} catch (InterruptedException e) {
throw new CruiseControlException("Problem trying to execute command line process", e);
}
}
protected void parseStream(final InputStream stream, final P4CmdParser parser)
throws IOException {
String line;
final BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
while ((line = reader.readLine()) != null) {
if (line.startsWith("error:")) {
throw new IOException("Error reading P4 stream: P4 says: " + line);
} else if (line.startsWith("exit: 0")) {
LOG.debug("p4cmd: Found exit 0");
break;
} else if (line.startsWith("exit:")) {
// not an exit code of 0
LOG.error("p4cmd: Found exit " + line);
throw new IOException("Error reading P4 stream: P4 says: " + line);
} else if (line.startsWith("warning:")) {
parser.warning(line.substring(8));
} else if (line.startsWith("info:") || line.startsWith("info1:")) {
parser.info(line.substring(5));
} else if (line.startsWith("text:")) {
parser.text(line.substring(5));
}
}
if (line == null) {
throw new IOException("Error reading P4 stream: Unexpected EOF reached");
}
}
protected static interface P4CmdParser {
public void warning(String msg);
public void info(String msg);
public void text(String msg);
}
protected static class P4CmdParserAdapter implements P4CmdParser {
public void warning(final String msg) {
// empty
}
public void info(final String msg) {
// empty
}
public void text(final String msg) {
// empty
}
}
protected static class ParseChangelistNumbers extends P4CmdParserAdapter {
private final ArrayList<String> changelists = new ArrayList<String>();
public void info(final String msg) {
final StringTokenizer st = new StringTokenizer(msg);
st.nextToken(); // skip 'Change' text
changelists.add(st.nextToken());
}
public String[] getChangelistNumbers() {
final String[] changelistNumbers = new String[ 0 ];
return changelists.toArray(changelistNumbers);
}
}
protected static class ParseOutputParam extends P4CmdParserAdapter {
public ParseOutputParam(final String paramName) {
this.paramName = paramName;
}
private final String paramName;
private final List<String> values = new ArrayList<String>();
public void info(final String msg) {
final String m = msg.trim();
if (m.startsWith(paramName)) {
final String m2 = m.substring(paramName.length()).trim();
values.add(m2);
}
}
public String[] getValues() {
final String[] v = new String[ 0 ];
return values.toArray(v);
}
}
}