/******************************************************************************** * 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.Arrays; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.StringTokenizer; 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.CommandlineUtil; import net.sourceforge.cruisecontrol.util.ValidationHelper; import org.apache.log4j.Logger; import org.jdom.Element; /** * This class implements the SourceControlElement methods for ClearCase UCM. * * @author <a href="mailto:kevin.lee@buildmeister.com">Kevin Lee</a> * @author Alex Batlin */ public class UCM implements SourceControl { private static final Logger LOG = Logger.getLogger(UCM.class); private String stream; private String viewPath; private boolean multiVob; private boolean contributors = true; private boolean rebases = false; private final SourceControlProperties properties = new SourceControlProperties(); private final SimpleDateFormat inputDateFormat = new SimpleDateFormat("dd-MMMM-yyyy.HH:mm:ss"); private final SimpleDateFormat outputDateFormat = new SimpleDateFormat("yyyyMMdd.HHmmss"); private String pvob; /** * 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 = "@#@#@#@#@#@#@#@#@#@#@#@"; public Map<String, String> getProperties() { return properties.getPropertiesAndReset(); } public void validate() throws CruiseControlException { ValidationHelper.assertIsSet(stream, "stream", this.getClass()); ValidationHelper.assertIsSet(viewPath, "viewpath", this.getClass()); if (isRebases()) { ValidationHelper.assertIsSet(pvob, "pvob", this.getClass()); } } /** * get which stream is being checked * * @return the name of the stream being checked */ public String getStream() { return stream; } /** * set the stream to check for changes * * @param stream * the stream to be checked (via its underlying branch) */ public void setStream(String stream) { this.stream = stream; } /** * get the starting point path in a view to check for changes * * @return path inside a view */ public String getViewPath() { return viewPath; } /** * set the starting point path in a view to check for changes * * @param viewPath * path inside a view */ public void setViewPath(String viewPath) { this.viewPath = viewPath; } /** * get whether the view contains multiple vobs * * @return true, if the view contains multiple vobs, else false */ public boolean isMultiVob() { return this.multiVob; } /** * set whether the view contains multiple vobs * * @param multiVob boolean indicating whether the view contains multiple vobs */ public void setMultiVob(boolean multiVob) { this.multiVob = multiVob; } /** * Set the name of the pvob to use for queries. * * @param pvob * the pvob */ public void setPvob(String pvob) { this.pvob = pvob; } /** * Get the name of the pvob to use for queries. * * @return The name of the pvob */ public String getPvob() { return this.pvob; } /** * Gets whether rebases are to be reported as changes. * * @return true, if rebases are to be reported, else false */ public boolean isRebases() { return this.rebases; } /** * Sets whether rebases of the integration stream are reported as changes. * * @param rebases * boolean indicating whether rebases are to be reported as changes */ public void setRebases(boolean rebases) { this.rebases = rebases; } /** * get whether contributors are to be found * * @return true, if contributors are to be found, else false */ public boolean isContributors() { return contributors; } /** * set whether contributors are to be found * * @param contributors * boolean indicating whether contributors are to be found */ public void setContributors(boolean contributors) { this.contributors = contributors; } /** * set the name of the property that will be set if modifications are found * * @param property * The name of the property to set */ public void setProperty(String property) { properties.assignPropertyName(property); } /** * 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) { final String lastBuildDate = inputDateFormat.format(lastBuild); final String nowDate = inputDateFormat.format(now); properties.put("ucmlastbuild", lastBuildDate); properties.put("ucmnow", nowDate); List<Modification> mods = new ArrayList<Modification>(); try { final HashMap<String, String> activityNames = collectActivitiesSinceLastBuild(lastBuildDate); if (activityNames.size() == 0) { return mods; } mods = describeAllActivities(activityNames); } catch (Exception e) { LOG.error("Command failed to execute succesfully", e); } if (this.isRebases()) { try { final Commandline commandline = buildDetectRebasesCommand(lastBuildDate); commandline.setWorkingDirectory(viewPath); final InputStream cmdStream = CommandlineUtil.streamOutput(commandline); try { mods.addAll(parseRebases(cmdStream)); } finally { cmdStream.close(); } } catch (Exception e) { LOG.error("Error in executing the Clear Case command : ", e); } } // If modifications were found, set the property if (!mods.isEmpty()) { properties.modificationFound(); } return mods; } /** * @param lastBuildDate last build date * @return all the activities on the stream since the last build date */ private HashMap<String, String> collectActivitiesSinceLastBuild(final String lastBuildDate) { LOG.debug("Last build time was: " + lastBuildDate); final HashMap<String, String> activityMap = new HashMap<String, String>(); final Commandline commandLine = buildListStreamCommand(lastBuildDate); LOG.debug("Executing: " + commandLine); try { commandLine.setWorkingDirectory(viewPath); final InputStream cmdStream = CommandlineUtil.streamOutput(commandLine); final InputStreamReader isr = new InputStreamReader(cmdStream); final BufferedReader br = new BufferedReader(isr); try { String line = br.readLine(); while ((line != null) && (!line.equals(""))) { final String[] details = getDetails(line); if (details[0].equals("mkbranch") || details[0].equals("rmbranch") || details[0].equals("rmver")) { // if type is create/remove branch then skip } else { final String activityName = details[1]; final String activityDate = details[2]; // assume the latest change for an activity is listed first if (!activityMap.containsKey(activityName)) { LOG.debug("Found activity name: " + activityName + "; date: " + activityDate); activityMap.put(activityName, activityDate); } } line = br.readLine(); } } finally { br.close(); cmdStream.close(); } } catch (IOException e) { LOG.error("IO Error executing ClearCase lshistory command", e); } catch (CruiseControlException e) { LOG.error("Interrupt Error executing ClearCase lshistory command", e); } return activityMap; } private String[] getDetails(String line) { // replacing line.split("~#~") for jdk 1.3 ArrayList<String> details = new ArrayList<String>(); String delimiter = "~#~"; int startIndex = 0; int index = 0; while (index != -1) { String detail; index = line.indexOf(delimiter, startIndex); if (index == -1) { detail = line.substring(startIndex, line.length()); } else { detail = line.substring(startIndex, index); } details.add(detail); startIndex = index + delimiter.length(); } return details.toArray(new String[details.size()]); } /** * @param lastBuildDate last build date * @return a command to get all the activities on the specified stream */ public Commandline buildListStreamCommand(final String lastBuildDate) { final Commandline commandLine = new Commandline(); if (isMultiVob()) { try { commandLine.setWorkingDirectory(getViewPath()); } catch (CruiseControlException e) { LOG.error("Error in setting workdirectory", e); } } commandLine.setExecutable("cleartool"); commandLine.createArgument("lshistory"); commandLine.createArguments("-branch", getStream()); if (isMultiVob()) { commandLine.createArgument("-avobs"); } else { commandLine.createArgument("-r"); } commandLine.createArgument("-nco"); commandLine.createArguments("-since", lastBuildDate); commandLine.createArguments("-fmt", "%o~#~%[activity]Xp~#~%Nd\n"); if (!isMultiVob()) { commandLine.createArgument(getViewPath()); } return commandLine; } /** * @param activityNames all the activities on the stream since the last build date * @return all the activities on the stream since the last build date */ private List<Modification> describeAllActivities(final HashMap<String, String> activityNames) { final ArrayList<Modification> activityList = new ArrayList<Modification>(); for (Map.Entry<String, String> activity : activityNames.entrySet()) { final String activityID = activity.getKey(); final String activityDate = activity.getValue(); final UCMModification activityMod = describeActivity(activityID, activityDate); activityList.add(activityMod); // check for contributor activities if (activityMod.comment.startsWith("deliver ") && isContributors()) { final List<String> contribList = describeContributors(activityID); for (final String contribName : contribList) { final UCMModification contribMod = describeActivity(contribName, activityDate); // prefix type to make it stand out in Build Results report contribMod.type = "contributor"; LOG.debug("Found contributor name: " + contribName + "; date: " + activityDate); activityList.add(contribMod); } } } return activityList; } /** * @param activityID stream ID * @param activityDate activity date * @return all the activities on the stream since the last build date */ private UCMModification describeActivity(final String activityID, final String activityDate) { final UCMModification mod = new UCMModification(); final Commandline commandLine = buildDescribeActivityCommand(activityID); LOG.debug("Executing: " + commandLine); try { commandLine.setWorkingDirectory(viewPath); final InputStream cmdStream = CommandlineUtil.streamOutput(commandLine); final InputStreamReader isr = new InputStreamReader(cmdStream); final BufferedReader br = new BufferedReader(isr); try { String line = br.readLine(); while ((line != null) && (!line.equals(""))) { final String[] details = getDetails(line); try { mod.modifiedTime = outputDateFormat.parse(activityDate); } catch (ParseException e) { LOG.error("Error parsing modification date"); mod.modifiedTime = new Date(); } mod.type = "activity"; // counter for UCM without ClearQuest if (details[0].equals("")) { mod.revision = details[3]; } else { mod.revision = details[0]; } mod.crmtype = details[1]; mod.userName = details[2]; mod.comment = details[3]; line = br.readLine(); } } finally { br.close(); cmdStream.close(); } } catch (IOException e) { LOG.error("IO Error executing ClearCase describe command", e); } catch (CruiseControlException e) { LOG.error("Interrupt error executing ClearCase describe command", e); } return mod; } /** * @param activityID a stream id * @return a command to get all the activities on the specified stream */ public Commandline buildDescribeActivityCommand(final String activityID) { final Commandline commandLine = new Commandline(); commandLine.setExecutable("cleartool"); commandLine.createArgument("describe"); commandLine.createArguments("-fmt", "%[crm_record_id]p~#~%[crm_record_type]p~#~%u~#~%[headline]p~#~"); commandLine.createArgument(activityID); return commandLine; } /** * @param activityName activity name * @return all the activities on the stream since the last build date */ private List<String> describeContributors(final String activityName) { final ArrayList<String> contribList = new ArrayList<String>(); final Commandline commandLine = buildListContributorsCommand(activityName); LOG.debug("Executing: " + commandLine); try { commandLine.setWorkingDirectory(viewPath); final InputStream cmdStream = CommandlineUtil.streamOutput(commandLine); final InputStreamReader isr = new InputStreamReader(cmdStream); final BufferedReader br = new BufferedReader(isr); try { String line; while ((line = br.readLine()) != null) { final String[] contribs = splitOnSpace(line); contribList.addAll(Arrays.asList(contribs)); } } finally { br.close(); cmdStream.close(); } } catch (IOException e) { LOG.error("IO Error executing ClearCase describe contributors command", e); } catch (CruiseControlException e) { LOG.error("Interrupt Error executing ClearCase describe contributors command", e); } return contribList; } private String[] splitOnSpace(final String string) { return string.split(" "); } /** * @param activityID stream ID * @return a command to get all the activities on the specified stream */ public Commandline buildListContributorsCommand(final String activityID) { final Commandline commandLine = new Commandline(); commandLine.setExecutable("cleartool"); commandLine.createArgument("describe"); commandLine.createArguments("-fmt", "%[contrib_acts]Xp"); commandLine.createArgument(activityID); return commandLine; } protected Commandline buildDetectRebasesCommand(final String lastBuildDate) { final Commandline commandLine = new Commandline(); commandLine.setExecutable("cleartool"); commandLine.createArgument().setValue("lshistory"); commandLine.createArgument().setValue("-since"); commandLine.createArgument().setValue(lastBuildDate); commandLine.createArgument().setValue("-minor"); commandLine.createArgument().setValue("-fmt"); final String format = "%u" + DELIMITER + "%Nd" + DELIMITER + "%o" + DELIMITER + "%Nc" + END_OF_STRING_DELIMITER + "\\n"; commandLine.createArgument().setValue(format); commandLine.createArgument().setValue("stream:" + stream + "@" + pvob); return commandLine; } /** * Parses the given input stream to construct the modifications list. The stream is expected to be the result of * listing the history of a UCM stream. Rebases are then detected by delegating to * {@link #parseRebaseEntry(String)}. * Package-private to make it available to the unit test. * * @param input * the stream to parse * @return a list of modification elements * @exception IOException if an IO error occurs */ List<Modification> parseRebases(final InputStream input) throws IOException { final ArrayList<Modification> modifications = new ArrayList<Modification>(); final BufferedReader reader = new BufferedReader(new InputStreamReader(input)); try { final String ls = System.getProperty("line.separator"); String line; StringBuffer lines = new StringBuffer(); while ((line = reader.readLine()) != null) { if (lines.length() != 0) { lines.append(ls); } lines.append(line); Modification mod = null; if (lines.indexOf(END_OF_STRING_DELIMITER) > -1) { mod = parseRebaseEntry(lines.substring(0, lines.indexOf(END_OF_STRING_DELIMITER))); lines = new StringBuffer(); } if (mod != null) { modifications.add(mod); } } } finally { reader.close(); } return modifications; } /** * Parses a single line from the reader. Each line contains a signe revision with the format : <br> * username#~#date_of_revision#~#operation_type#~#comments <br> * <p> * This method looks for operations of type rmhlink and mkhlink where the hyperlink name begins with "UseBaseline". * These represent changes in the baseline dependencies of the stream. * </p> * * @param line * the line to parse * @return a modification element corresponding to the given line */ Modification parseRebaseEntry(final String line) { LOG.debug("parsing entry: " + line); String[] tokens = tokenizeEntry(line); if (tokens == null) { return null; } String username = tokens[0].trim(); String timeStamp = tokens[1].trim(); String operationType = tokens[2].trim(); String comment = tokens[3].trim(); Modification mod = null; // Rebases show up as mkhlink and rmhlink operations if (operationType.equals("mkhlink") || operationType.equals("rmhlink")) { // Parse the hyperlink name out of the comment field, then // get more information on that hyperlink mod = new Modification(); String linkName = parseLinkName(comment); // If this isn't a "UseBaseline" hyperlink, we're not interested if (!linkName.startsWith("UseBaseline")) { return null; } Hyperlink link = getHyperlink(linkName); StringBuffer modComment = new StringBuffer(); mod.type = "ucmdependency"; if (operationType.equals("mkhlink")) { modComment.append("Added dependency"); } else { modComment.append("Removed dependency"); } if (link.getFrom().length() > 0) { modComment.append(" of "); modComment.append(link.getFrom()); } if (link.getTo().length() > 0) { modComment.append(" on "); modComment.append(link.getTo()); mod.revision = link.getTo(); } else { // Don't know what the revision was to mod.revision = ""; } mod.comment = modComment.toString(); mod.userName = username; try { mod.modifiedTime = outputDateFormat.parse(timeStamp); } catch (ParseException e) { LOG.error("Error parsing modification date", e); mod.modifiedTime = new Date(); } properties.modificationFound(); } return mod; } private Hyperlink getHyperlink(String linkName) { Commandline commandline = buildGetHyperlinkCommandline(linkName); try { commandline.setWorkingDirectory(viewPath); InputStream cmdStream = CommandlineUtil.streamOutput(commandline); Hyperlink link = null; try { link = parseHyperlinkDescription(cmdStream); } finally { cmdStream.close(); } return link; } catch (Exception e) { LOG.error("Error in executing the Clear Case command : ", e); return new Hyperlink(); } } protected Commandline buildGetHyperlinkCommandline(String linkName) { Commandline commandline = new Commandline(); commandline.setExecutable("cleartool"); commandline.createArgument().setValue("describe"); commandline.createArgument().setValue("hlink:" + linkName + "@" + pvob); return commandline; } Hyperlink parseHyperlinkDescription(final InputStream input) throws IOException { final BufferedReader reader = new BufferedReader(new InputStreamReader(input)); try { String lastLine = ""; String line = reader.readLine(); // If the hyperlink wasn't found, cleartool will return no output, giving // us an empty stream. This will end up returning an empty string. while (line != null) { lastLine = line; line = reader.readLine(); } final Hyperlink link = new Hyperlink(); final StringTokenizer tokens = new StringTokenizer(lastLine, " "); if (!tokens.hasMoreTokens()) { return link; } // Discard the first one, that's the link name tokens.nextToken(); if (!tokens.hasMoreTokens()) { return link; } link.setFrom(tokens.nextToken()); // Discard "->" if (!tokens.hasMoreTokens()) { return link; } tokens.nextToken(); if (!tokens.hasMoreTokens()) { return link; } link.setTo(tokens.nextToken()); return link; } finally { reader.close(); } } String parseLinkName(String comment) { // Parse on spaces and quotes (to eliminate them) StringTokenizer tokens = new StringTokenizer(comment, " \""); String link = ""; String token = ""; // The next-to-last token should contain the hyperlink name while (tokens.hasMoreTokens()) { link = token; token = tokens.nextToken(); } int index = link.lastIndexOf('@'); if (index != -1) { // Remove the VOB-qualifier from the end. We'll add it back ourselves return link.substring(0, link.lastIndexOf('@')); } else { // Return it unmodified return link; } } private String[] tokenizeEntry(String line) { int maxTokens = 4; int minTokens = maxTokens - 1; // comment may be absent. String[] tokens = new String[maxTokens]; Arrays.fill(tokens, ""); int tokenIndex = 0; for (int oldIndex = 0, i = line.indexOf(DELIMITER, 0); true; oldIndex = i + DELIMITER.length(), i = line .indexOf(DELIMITER, oldIndex), tokenIndex++) { if (tokenIndex > maxTokens) { LOG.debug("Too many tokens; skipping entry"); return null; } if (i == -1) { tokens[tokenIndex] = line.substring(oldIndex); break; } else { tokens[tokenIndex] = line.substring(oldIndex, i); } } if (tokenIndex < minTokens) { LOG.debug("Not enough tokens; skipping entry"); return null; } return tokens; } /** * Class to represent ClearCase hyperlinks. */ class Hyperlink { private String from = ""; private String to = ""; public String getFrom() { return this.from; } public void setFrom(String from) { this.from = from; } public String getTo() { return this.to; } public void setTo(String to) { this.to = to; } } /** * class to hold UCMModifications */ private static class UCMModification extends Modification { private static final String TAGNAME_CRMTYPE = "crmtype"; public String crmtype; public int compareTo(final Modification o) { UCMModification modification = (UCMModification) o; return getActivitityNumber() - modification.getActivitityNumber(); } public boolean equals(Object o) { if (o == null || !(o instanceof UCMModification)) { return false; } UCMModification modification = (UCMModification) o; return getActivitityNumber() == modification.getActivitityNumber(); } public int hashCode() { return getActivitityNumber(); } private int getActivitityNumber() { return Integer.parseInt(revision); } UCMModification() { super("ucm"); } public Element toElement() { Element modificationElement = super.toElement(); Element crmtypeElement = new Element(TAGNAME_CRMTYPE); crmtypeElement.addContent(crmtype); modificationElement.addContent(crmtypeElement); return modificationElement; } } }