/********************************************************************************
* CruiseControl, a Continuous Integration Toolkit
* Copyright (c) 2001-2003, 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.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;
import net.sourceforge.cruisecontrol.CruiseControlException;
import net.sourceforge.cruisecontrol.Modification;
import net.sourceforge.cruisecontrol.SourceControl;
import net.sourceforge.cruisecontrol.util.CommandExecutor;
import net.sourceforge.cruisecontrol.util.Commandline;
import net.sourceforge.cruisecontrol.util.DiscardConsumer;
import net.sourceforge.cruisecontrol.util.IO;
import net.sourceforge.cruisecontrol.util.StreamConsumer;
import net.sourceforge.cruisecontrol.util.StreamLogger;
import net.sourceforge.cruisecontrol.util.StreamPumper;
import net.sourceforge.cruisecontrol.util.ValidationHelper;
import org.apache.log4j.Logger;
import org.jdom.Element;
/**
* This class implements the SourceControlElement methods for a P4 depot. The call to CVS is assumed to work without any
* setup. This implies that if the authentication type is pserver the call to cvs login should be done prior to calling
* this class. <p> P4Element depends on the optional P4 package delivered with Ant v1.3. But since it probably doesn't
* make much sense using the P4Element without other P4 support it shouldn't be a problem. <p> P4Element sets the
* property ${p4element.change} with the latest changelist number or the changelist with the latest date. This should
* then be passed into p4sync or other p4 commands.
*
* @author <a href="mailto:niclas.olofsson@ismobile.com">Niclas Olofsson - isMobile.com</a>
* @author <a href="mailto:jcyip@thoughtworks.com">Jason Yip</a>
* @author Tim McCune
* @author J D Glanville
* @author Patrick Conant Copyright (c) 2005 Hewlett-Packard Development Company, L.P.
* Licensed under the CruiseControl BSD license
* @author John Lussmyer
*/
public class P4 implements SourceControl {
private static final Logger LOG = Logger.getLogger(P4.class);
private String p4Port;
private String p4Client;
private String p4User;
private String p4View;
private String p4Passwd;
private boolean correctForServerTime = true;
private boolean useP4Email = true;
private final SimpleDateFormat p4RevisionDateFormatter = new SimpleDateFormat("yyyy/MM/dd:HH:mm:ss");
private final SourceControlProperties properties = new SourceControlProperties();
private static final String SERVER_DATE = "Server date: ";
private static final String P4_SERVER_DATE_FORMAT = "yyyy/MM/dd HH:mm:ss";
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;
}
/**
* Indicates whether to correct for time differences between the p4 server and the CruiseControl server. Setting the
* flag to "true" will correct for both time zone differences and for non-synchronized system clocks.
* @param flag "true" will correct for both time zone differences and for non-synchronized system clocks.
*/
public void setCorrectForServerTime(boolean flag) {
correctForServerTime = flag;
}
/**
* Sets if the Email address for the user should be retrieved from Perforce.
*
* @param flag
* true to retrieve email addresses from perforce.
*/
public void setUseP4Email(boolean flag) {
useP4Email = flag;
}
public void setProperty(String propertyName) {
properties.assignPropertyName(propertyName);
}
public Map<String, String> getProperties() {
return properties.getPropertiesAndReset();
}
public void validate() throws CruiseControlException {
ValidationHelper.assertIsSet(p4Port, "port", this.getClass());
ValidationHelper.assertIsSet(p4Client, "client", this.getClass());
ValidationHelper.assertIsSet(p4User, "user", this.getClass());
ValidationHelper.assertIsSet(p4View, "view", this.getClass());
ValidationHelper.assertNotEmpty(p4Passwd, "passwd", this.getClass());
}
/**
* Get a List of modifications detailing all the changes between now and the last build. Return this as an element.
* It is not necessary for sourcecontrols to actually do anything other than returning a chunk of XML data back.
*
* @param lastBuild
* time of last build
* @param now
* time this build started
* @return a list of XML elements that contains data about the modifications that took place. If no changes, this
* method returns an empty list.
*/
public List<Modification> getModifications(final Date lastBuild, final Date now) {
List<Modification> mods = new ArrayList<Modification>();
try {
final String[] changelistNumbers = collectChangelistSinceLastBuild(lastBuild, now);
if (changelistNumbers.length == 0) {
return mods;
}
mods = describeAllChangelistsAndBuildOutput(changelistNumbers);
} catch (Exception e) {
LOG.error("Log command failed to execute succesfully", e);
}
if (!mods.isEmpty()) {
properties.modificationFound();
}
return mods;
}
private List<Modification> describeAllChangelistsAndBuildOutput(final String[] changelistNumbers) throws Exception {
final Commandline command = buildDescribeCommand(changelistNumbers);
LOG.debug(command.toString());
final Process p = command.execute();
final Thread error = logErrorStream(p.getErrorStream());
final InputStream p4Stream = p.getInputStream();
final List<Modification> mods = parseChangeDescriptions(p4Stream);
getRidOfLeftoverData(p4Stream);
// Get the Email address of the user for each changelist
if ((mods.size() > 0) && useP4Email) {
getEmailAddresses(mods);
}
p.waitFor();
error.join();
IO.close(p);
return mods;
}
/**
* Get the Email Address of the users who submitted the change lists.
*
* @param mods
* List of P4Modification structures
* @throws IOException if something breaks
* @throws InterruptedException if something breaks
*/
private void getEmailAddresses(final List<Modification> mods) throws IOException, InterruptedException {
final Iterator<Modification> iter = mods.iterator();
final Map<String, String> users = new HashMap<String, String>();
while (iter.hasNext()) {
final P4Modification change = (P4Modification) iter.next();
if ((change.userName != null) && (change.userName.length() > 0)) {
change.emailAddress = users.get(change.userName);
if (change.emailAddress == null) {
change.emailAddress = getUserEmailAddress(change.userName);
users.put(change.userName, change.emailAddress);
}
}
}
}
/**
* Get the Email Address for the given P4 User
*
* @param username
* Perforce user name
* @return User Email address if available
* @throws IOException if something breaks
* @throws InterruptedException if something breaks
*/
private String getUserEmailAddress(final String username) throws IOException, InterruptedException {
String emailaddr = null;
final Commandline command = buildUserCommand(username);
LOG.debug(command.toString());
final Process p = command.execute();
logErrorStream(p.getErrorStream());
final InputStream p4Stream = p.getInputStream();
final BufferedReader reader = new BufferedReader(new InputStreamReader(p4Stream));
// Find first Changelist item if there is one.
String line;
while ((line = readToNotPast(reader, "info: Email:", "I really don't care")) != null) {
final StringTokenizer st = new StringTokenizer(line);
try {
st.nextToken(); // skip 'info:' text
st.nextToken(); // skip 'Email:' text
emailaddr = st.nextToken();
} catch (NoSuchElementException ex) {
// No email address given
}
}
getRidOfLeftoverData(p4Stream);
p.waitFor();
IO.close(p);
return (emailaddr);
}
private String[] collectChangelistSinceLastBuild(final Date lastBuild, final Date now) throws Exception {
final Commandline command = buildChangesCommand(lastBuild, now);
LOG.debug(command.toString());
final Process p = command.execute();
final Thread error = logErrorStream(p.getErrorStream());
final InputStream p4Stream = p.getInputStream();
final String[] changelistNumbers = parseChangelistNumbers(p4Stream);
p.waitFor();
error.join();
IO.close(p);
return changelistNumbers;
}
private void getRidOfLeftoverData(final InputStream stream) {
new StreamPumper(stream, new DiscardConsumer()).run();
}
protected String[] parseChangelistNumbers(final InputStream is) throws IOException {
final ArrayList<String> changelists = new ArrayList<String>();
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
String line;
while ((line = reader.readLine()) != null) {
if (line.startsWith("error:")) {
throw new IOException("Error reading P4 stream: P4 says: " + line);
} else if (line.startsWith("exit: 1")) {
throw new IOException("Error reading P4 stream: P4 says: " + line);
} else if (line.startsWith("exit: 0")) {
break;
} else if (line.startsWith("info:")) {
StringTokenizer st = new StringTokenizer(line);
st.nextToken(); // skip 'info:' text
st.nextToken(); // skip 'Change' text
changelists.add(st.nextToken());
}
}
if (line == null) {
throw new IOException("Error reading P4 stream: Unexpected EOF reached");
}
return changelists.toArray(new String[changelists.size()]);
}
protected List<Modification> parseChangeDescriptions(final InputStream is) throws Exception {
int serverOffset = 0;
if (correctForServerTime) {
serverOffset = (int) calculateServerTimeOffset();
}
final ArrayList<Modification> changelists = new ArrayList<Modification>();
final BufferedReader reader = new BufferedReader(new InputStreamReader(is));
// Find first Changelist item if there is one.
String line;
while ((line = readToNotPast(reader, "text: Change", "exit:")) != null) {
final P4Modification changelist = new P4Modification();
if (line.startsWith("error:")) {
throw new IOException("Error reading P4 stream: P4 says: " + line);
} else if (line.startsWith("exit: 1")) {
throw new IOException("Error reading P4 stream: P4 says: " + line);
} else if (line.startsWith("exit: 0")) {
return changelists;
} else if (line.startsWith("text: Change")) {
final StringTokenizer st = new StringTokenizer(line);
st.nextToken(); // skip 'text:' text
st.nextToken(); // skip 'Change' text
changelist.revision = st.nextToken();
st.nextToken(); // skip 'by' text
// split user@client
final StringTokenizer st2 = new StringTokenizer(st.nextToken(), "@");
changelist.userName = st2.nextToken();
changelist.client = st2.nextToken();
st.nextToken(); // skip 'on' text
final String date = st.nextToken() + ":" + st.nextToken();
try {
final Calendar cal = Calendar.getInstance();
cal.setTime(p4RevisionDateFormatter.parse(date));
cal.add(Calendar.MILLISECOND, -serverOffset);
changelist.modifiedTime = cal.getTime();
} catch (ParseException xcp) {
changelist.modifiedTime = new Date();
}
}
reader.readLine(); // get past a 'text:'
final StringBuilder descriptionBuffer = new StringBuilder();
// Use this since we don't want the final (empty) line
String previousLine = null;
line = reader.readLine();
while (line != null && line.startsWith("text:") && !line.startsWith("text: Affected files ...")) {
if (previousLine != null) {
if (descriptionBuffer.length() > 0) {
descriptionBuffer.append('\n');
}
descriptionBuffer.append(previousLine);
}
try {
previousLine = line.substring(5).trim();
} catch (Exception e) {
LOG.error("Error parsing Perforce description, line that caused problem was: [" + line + "]");
}
line = reader.readLine();
}
changelist.comment = descriptionBuffer.toString();
// Ok, read affected files if there are any.
if (line != null) {
reader.readLine(); // read past next 'text:'
line = readToNotPast(reader, "info1:", "text:");
while (line != null && line.startsWith("info1:")) {
String fileName = line.substring(7, line.lastIndexOf("#"));
Modification.ModifiedFile affectedFile = changelist.createModifiedFile(fileName, null);
affectedFile.action = line.substring(line.lastIndexOf(" ") + 1);
affectedFile.revision = line.substring(line.lastIndexOf("#") + 1, line.lastIndexOf(" "));
line = readToNotPast(reader, "info1:", "text:");
}
}
changelists.add(changelist);
}
return changelists;
}
private Thread logErrorStream(final InputStream is) {
final Thread errorThread = new Thread(StreamLogger.getWarnPumper(LOG, is));
errorThread.start();
return errorThread;
}
/**
* p4 -s [-c client] [-p port] [-u user] changes -s submitted [view@lastBuildTime@now]
*
* @param lastBuildTime last build date
* @param now current build date
* @return p4 -s [-c client] [-p port] [-u user] changes -s submitted [view@lastBuildTime@now]
* @throws CruiseControlException if somtething breaks
*/
public Commandline buildChangesCommand(Date lastBuildTime, Date now)
throws CruiseControlException {
// If the Perforce server time is different from the CruiseControl
// server time, correct the parameter dates for the difference.
if (correctForServerTime) {
int offset = (int) calculateServerTimeOffset();
final Calendar cal = Calendar.getInstance();
cal.setTime(lastBuildTime);
cal.add(Calendar.MILLISECOND, offset);
lastBuildTime = cal.getTime();
cal.setTime(now);
cal.add(Calendar.MILLISECOND, offset);
now = cal.getTime();
} else {
LOG.debug("No server time offset determined.");
}
final Commandline commandLine = buildBaseP4Command();
commandLine.createArgument("changes");
commandLine.createArguments("-s", "submitted");
commandLine.createArgument(p4View + "@" + p4RevisionDateFormatter.format(lastBuildTime) + ",@"
+ p4RevisionDateFormatter.format(now));
return commandLine;
}
/**
* @param changelistNumbers change list numbers
* @return p4 -s [-c client] [-p port] [-u user] describe -s [change number]
*/
public Commandline buildDescribeCommand(final String[] changelistNumbers) {
final Commandline commandLine = buildBaseP4Command();
// execP4Command("describe -s " + changeNumber.toString(),
commandLine.createArgument("describe");
commandLine.createArgument("-s");
for (final String changelistNumber : changelistNumbers) {
commandLine.createArgument(changelistNumber);
}
return commandLine;
}
/**
* @param username user name
* @return p4 -s [-c client] [-p port] [-u user] user -o [username]
*/
public Commandline buildUserCommand(final String username) {
final Commandline commandLine = buildBaseP4Command();
commandLine.createArgument("user");
commandLine.createArguments("-o", username);
return commandLine;
}
/**
* Calculate the difference in time between the Perforce server and the CruiseControl server. A negative time
* difference indicates that the Perforce server time is later than CruiseControl server (e.g. Perforce in New York,
* CruiseControl in San Francisco). A positive offset indicates that the Perforce server time is before the
* CruiseControl server.
*
* @return the difference in time between the Perforce server and the CruiseControl server.
* @throws CruiseControlException if something breaks
*/
protected long calculateServerTimeOffset() throws CruiseControlException {
final ServerInfoConsumer serverInfo = new ServerInfoConsumer();
final CommandExecutor executor = new CommandExecutor(buildInfoCommand());
executor.logErrorStreamTo(LOG);
executor.setOutputConsumer(serverInfo);
executor.executeAndWait();
return serverInfo.getOffset();
}
Commandline buildInfoCommand() {
final Commandline command = buildBaseP4Command(false);
command.createArgument("info");
return command;
}
private Commandline buildBaseP4Command() {
final boolean prependField = true;
return buildBaseP4Command(prependField);
}
private Commandline buildBaseP4Command(final boolean prependField) {
final Commandline commandLine = new Commandline();
commandLine.setExecutable("p4");
if (prependField) {
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;
}
/**
* This is a modified version of the one in the CVS element. I found it far more useful if you actually return
* either or, because otherwise it would be darn hard to use in places where I actually need the notPast line. Or
* did I misunderstand something?
* @param reader reader
* @param beginsWith beginsWith
* @param notPast notPast
* @return a string
* @throws IOException if something breaks.
*/
private String readToNotPast(final BufferedReader reader, final String beginsWith, final String notPast)
throws IOException {
String nextLine = reader.readLine();
// (!A && !B) || (!A && !C) || (!B && !C)
// !A || !B || !C
while (!(nextLine == null || nextLine.startsWith(beginsWith) || nextLine.startsWith(notPast))) {
nextLine = reader.readLine();
}
return nextLine;
}
private static class P4Modification extends Modification {
public String client;
@Override
public int compareTo(final Modification o) {
P4Modification modification = (P4Modification) o;
return getChangelistNumber() - modification.getChangelistNumber();
}
@Override
public boolean equals(final Object o) {
if (o == null || !(o instanceof P4Modification)) {
return false;
}
final P4Modification modification = (P4Modification) o;
return getChangelistNumber() == modification.getChangelistNumber();
}
@Override
public int hashCode() {
return getChangelistNumber();
}
private int getChangelistNumber() {
return Integer.parseInt(revision);
}
P4Modification() {
super("p4");
}
@Override
public Element toElement() {
final Element element = super.toElement();
LOG.debug("client = " + client);
final Element clientElement = new Element("client");
clientElement.addContent(client);
element.addContent(clientElement);
return element;
}
}
static String getQuoteChar(final boolean isWindows) {
return isWindows ? "\"" : "'";
}
protected static class ServerInfoConsumer implements StreamConsumer {
private boolean found;
private long offset;
private final SimpleDateFormat p4ServerDateFormatter = new SimpleDateFormat(P4_SERVER_DATE_FORMAT);
private Date ccServerTime = new Date();
public void consumeLine(final String line) {
final Date p4ServerTime;
// Consume the full stream after we have found the offset
if (found) {
return;
}
if (line.startsWith(SERVER_DATE)) {
try {
String dateString = line.substring(SERVER_DATE.length(), SERVER_DATE.length()
+ P4_SERVER_DATE_FORMAT.length());
p4ServerTime = p4ServerDateFormatter.parse(dateString);
offset = p4ServerTime.getTime() - ccServerTime.getTime();
found = true;
} catch (ParseException pe) {
LOG.error("Unable to parse p4 server time from line \'" + line + "\'. " + pe.getMessage()
+ "; Proceeding without time offset.");
}
}
}
public long getOffset() {
LOG.info("Perforce server time offset: " + offset + " ms");
return offset;
}
}
}