/******************************************************************************** * 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.File; import java.io.IOException; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; 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.ExamplesFile; import net.sourceforge.cruisecontrol.gendoc.annotations.Optional; import net.sourceforge.cruisecontrol.gendoc.annotations.Required; import net.sourceforge.cruisecontrol.gendoc.annotations.SkipDoc; import net.sourceforge.cruisecontrol.util.ManagedCommandline; import net.sourceforge.cruisecontrol.util.Util; import net.sourceforge.cruisecontrol.util.ValidationHelper; import org.apache.log4j.Logger; /** * Checks for modifications made to a Telelogic CM Synergy repository. It does * this by examining a provided reference project, getting the tasks from all * folders in that project, and checking the completion time of those tasks * against the last build. * * @author <a href="mailto:rjmpsmith@gmail.com">Robert J. Smith</a> */ @DescriptionFile @ExamplesFile public class CMSynergy implements SourceControl { private static final long serialVersionUID = -65675636345070572L; /** * A delimiter used for data values returned from a CM Synergy query */ public static final String CCM_ATTR_DELIMITER = "@#@#@#@"; /** * A delimiter used to mark the end of a multi-lined result from a query */ private static final String CCM_END_OBJECT = "<<<#@#@#>>>"; /** * The default CM Synergy command line client executable */ public static final String CCM_EXE = "ccm"; /** * The environment variable used by CM Synergy to determine which backend * ccmSession to use when issuing commands. */ private static final String CCM_SESSION_VAR = "CCM_ADDR"; /** * The default CM Synergy session map file */ public static final String CCM_SESSION_FILE = System.getProperty("user.home") + File.separator + ".ccmsessionmap"; /** * An instance of the logging class */ private static final Logger LOG = Logger.getLogger(CMSynergy.class); /** * A collection of properties which will be passed to and set within the * builder. */ private final SourceControlProperties properties = new SourceControlProperties(); /** * The name of the property which will be set and passed to the builder if * any object has changed since the last build. */ private String property = "cc.ccm.haschanged"; /** * The version number delimeter used by the database with which this CM * Synergy session is connected. */ private String ccmDelimiter = "-"; /** * The URL for your installation of Change Synergy */ private String changeSynergyURL; /** * The CCM database with which we wish to connect */ private String ccmDb; /** * The CM Synergy executable used for executing commands. If not set, we * will use the default value "ccm". */ private String ccmExe; /** * The CM Synergy project spec (2 part name). */ private String projectSpec; /** * The instance number of the project. This is almost always "1", but might * need to be overridden if you are using DCM? */ private String projectInstance = "1"; /** * The CM Synergy project four part name we will use as a template to * determine if any new tasks have been completed. */ private String projectFourPartName; /** * If set to true, the contents of the folders contained within the * project's reconfigure properties will be updated before we query to find * new tasks. */ private boolean updateFolders = true; /** * The file which contains the mapping between CM Synergy session names and * IDs. */ private File sessionFile; /** * The name of the CM Synergy session to use. */ private String sessionName; /** * The date format as returned by your installation of CM Synergy. */ private String ccmDateFormat = "EEE MMM dd HH:mm:ss yyyy"; // Fri Dec 3 // 17:51:56 2004 /** * If set to true, the project will be reconfigured when changes are * detected. */ private boolean reconfigure = false; /** * Used in conjunction with reconfigure. If set to true, all subprojects * will be reconfigured when changes are detected. */ private boolean recurse = true; /** * If set to true, the time a task came into a reconfigure folder is used * to determine modified tasks instead of the tasks completion time. Works * for Synergy 6.3SP1 and newer only. */ private boolean useBindTime = false; /** * If set to true, the work area location will not be queried and passed to * the builder. */ private boolean ignoreWorkarea = false; /** * The locale used for parsing dates. */ private Locale locale; /** * The language used to set the locale for parsing CM Synergy dates. */ private String language = "en"; /** * A reusable commandline for issuing CM Synergy commands */ private ManagedCommandline cmd; /** * The country used to set the locale for parsing CM Synergy dates. */ private String country = "US"; /** * The number of modified tasks found */ private int numTasks; /** * The number of modified objects found */ private int numObjects; /** * Sets the name of the CM Synergy executable to use when issuing commands. * * @param ccmExe * the name of the CM Synergy executable */ @Description("The name of the CM Synergy command line client. If not provided, the " + "plugin will search the system path for an executable called \"ccm\" (or " + "\"ccm.exe\" for Windows).") @Optional public void setCcmExe(String ccmExe) { this.ccmExe = ccmExe; } /** * Sets the CM Synergy project spec to be used as a template for calculating * changes. The value set here can be accessed from within the build as the * property "cc.ccm.project". * * @param projectSpec * The project spec (in 2 part name format). */ @Description("The project spec (two part name) of the CM Synergy project in which " + "you wish to search for changes.") @Required public void setProject(String projectSpec) { this.projectSpec = projectSpec; } /** * Sets the project's instance value. This value will be used in any query * which involves the project. Defaults to "1". This default should work for * most people. You might, however, need to override this value when using * DCM? * * @param projectInstance * The instance number of the project. */ @Description("Used to set the project's instance value. As CM Synergy only allows a " + "single instance of a project object in any given database, this attribute " + "defaults to \"1\", and should not need to be changed by most users. " + "<strong>You might, however, need to set this value when using the DCM " + "(Distributed Change Management) feature of the tool - which appends the DB " + "name to the instance value.</strong>") @Optional public void setInstance(String projectInstance) { this.projectInstance = projectInstance; } /** * Sets the URL for your installation of Change Synergy. This is used to * create active links from the modification report to the Change Requests * associated with the modified tasks. If not set, the links will not be * created. If you wish to use this feature, you must also set the ccmdb * attribute to the remote location of the Synergy database. * * @param url * The URL of your ChangeSynergy installation */ @Description("If provided, an active link will be created from the build results web " + "page to any change requests associated with any new tasks. The format " + "should be \"http://server:port\". If you wish to use this option, you must " + "also set the \"ccmdb\" attribute.") @Optional public void setChangeSynergyURL(String url) { this.changeSynergyURL = url; } /** * Sets the remote Synergy database with which to connect. This is only * needed if you wish to create active links from the build results page to * your installation of Change Synergy. If you set this attribute, you must * also set the changesynergyurl attribute. * * @param db * The remote Synergy database with which to connect (e.g. * /ccmdb/mydb). */ @Description("Used in conjunction with changesynergyurl. This should be set to the " + "location of the database on the CM Synergy server. (e.g. \"/ccmdb/mydb\")") @Optional public void setCcmDb(String db) { this.ccmDb = db; } /** * Sets the value of the updateFolders attribute. * @param updateFolders If set to true, the * contents of the folders contained within the project's reconfigure * properties will be updated before we query to find new tasks. */ @Description("By default, the plugin will always refresh the reconfigure properties of " + "the given project before searching for changes. This allows any query based " + "folders to update themselves with new tasks. If you wish to disable this " + "feature (not recommended), you may set this attribute to false. <strong>This " + "feature will only work with CM Synergy version 6.3 and above. If you are " + "using an older version, you <em>must</em> set this option to false. In this " + "case, you'll want to use the <a href=\"#cmsynergybootstrapper\"><" + "cmsynergybootstrapper></a> as a workaround. Please see example 2 below " + "as well as the Wiki site for more information.</strong>") @Optional public void setUpdateFolders(boolean updateFolders) { this.updateFolders = updateFolders; } /** * Sets the file which contains the mapping between CM Synergy session names * and IDs. This file should be in the standard properties file format. Each * line should map one name to a CM Synergy session ID (as returned by the * "ccm status" command). * <p> * example: <br> * <br> * session1=localhost:65024:192.168.1.17 * * @param sessionFile * The session file */ @Description("The session file used by the <a href=\"#cmsynergysessionmonitor\"><" + "cmsynergysessionmonitor></a> to persist your CM Synergy session " + "information. If this attribute is not set, it defaults to the file " + "\".ccmsessionmap\" in your home directory.") @Optional public void setSessionFile(String sessionFile) { this.sessionFile = new File(sessionFile); } /** * Sets the name of the CM Synergy session to use with this plugin. This * name should appear in the specified session file. * * @param sessionName * The session name * * @see #setSessionFile(String) */ @Description("The session name of the CM Synergy session you wish to use. This name " + "must appear in the session file. If not set, the plugin will attempt to " + "use the default (current) session as returned by the \"ccm status\" command.") @Optional public void setSessionName(String sessionName) { this.sessionName = sessionName; } /** * Sets the date format used by your installation of CM Synergy. The format * string should use the syntax described in <code>SimpleDateFormat</code>. * The default is "EEE MMM dd HH:mm:ss yyyy" The value set here can be * accessed from within the build as the property "cc.ccm.dateformat". * * @param format * the date format */ @Description("The default (output) date format used by CM Synergy is \"EEE MMM dd " + "HH:mm:ss yyyy\" If you have customized this for your installation, you " + "must provide the correct format here. The format should follow the standard " + "defined in Java's SimpleDateFormat class.") @Optional public void setCcmDateFormat(String format) { this.ccmDateFormat = format; } /** * Sets the value of the reconfigure attribute. * @param reconfigure If set to true, the project * will be reconfigured when changes are detected. Default value is false. */ @Description("Disabled by default. If you set this option to true, the project (and " + "optionally any subprojects) will be automatically reconfigured when changes " + "are detected. This eliminates the need to handle this from within your build " + "scripts.") @Optional @Default("false") public void setReconfigure(boolean reconfigure) { this.reconfigure = reconfigure; } /** * Sets the value of the useBindtime attribute. * @param useBindTime If set to true, the time the * task came into the reconfigure folders is used to query the modifications * instead of the time the task was completed. Works * for Synergy 6.3SP1 and newer only. * Default value is false. */ @Description("If set to true, the time a task came into the reconfigure folder is used " + "to determine modified tasks instead of the tasks completion date. This is a " + "more precise query to find modifications since the last build when the " + "reconfiguration rules are based on other criteria e.g. the status of a " + "Change Synergy change request. <strong>This feature will only work with CM " + "Synergy version 6.3SP1 and above. If you are using an older version, you " + "<em>must</em> set this option to false (default).</strong>") @Optional public void setUseBindTime(boolean useBindTime) { this.useBindTime = useBindTime; } /** * Sets the value of the recurse attribute. Used in conjuction with the * reconfigure attribute. * @param recurse If set to true, all subprojects will also be * reconfigured when changes are detected. Default is true. */ @Description("Used in conjunction with the reconfigure option. If set to true (which is " + "the default) all subprojects will also be reconfigured.") @Optional @Default("true") public void setRecurse(boolean recurse) { this.recurse = recurse; } /** * Sets the value of the ignoreWorkarea attribute. * @param ignoreWorkarea If set to true, we will * not attempt to determine the location of the project's workarea, nor will * we pass the cc.ccm.workarea attribute to the builders. Default is false. */ @Description("By default, the plugin will query CM Synergy to determine the work area " + "location of the project. This location is then passed to the builders in " + "the property \"cc.ccm.workarea\". If you wish to disable this feature (not " + "recommended), you can set this attribute to <em>true</em>.") @Optional public void setIgnoreWorkarea(boolean ignoreWorkarea) { this.ignoreWorkarea = ignoreWorkarea; } /** * Sets the language used to create the locale for parsing CM Synergy dates. * The format should follow the ISO standard as specified by * <code>java.util.Locale</code>. The default is "en" (English). * * @param language * The language to use when creating the <code>Locale</code> */ @Description("If you have a non U.S. English installation of CM Synergy, you may " + "specify the ISO language code here. (e.g. fr, de, etc.)") @Optional public void setLanguage(String language) { this.language = language; } /** * Sets the country used to create the locale for parsing CM Synergy dates. * The format should follow the ISO standard as specified by * <code>java.util.Locale</code>. The default is "US" (United States). * * @param country * The ISO country code to use */ @Description("If you have a non U.S. English installation of CM Synergy, you may " + "specify the ISO country code here. (e.g. FR, DE, etc.)") @Optional public void setCountry(String country) { this.country = country; } public Map<String, String> getProperties() { return properties.getPropertiesAndReset(); } @Description("Added for compliance with the CruiseControl API. A property of this " + "name will be provided to the builders if any CM Synergy object has changed " + "since the last build. The default is cc.ccm.haschanged, and probably " + "shouldn't be altered.") @Optional @Default("cc.ccm.haschanged") public void setProperty(String property) { this.property = property; } public void validate() throws CruiseControlException { ValidationHelper.assertIsSet(projectSpec, "project", this.getClass()); } public List<Modification> getModifications(final Date lastBuild, final Date now) { // Create a Locale appropriate for this installation locale = new Locale(language, country); if (!locale.equals(Locale.US)) { LOG.info("Locale has been set to " + locale.toString()); } // Attempt to get the database delimiter cmd = createCcmCommand(ccmExe, sessionName, sessionFile); cmd.createArgument("delimiter"); try { cmd.execute(); cmd.assertExitCode(0); this.ccmDelimiter = cmd.getStdoutAsString().trim(); } catch (Exception e) { final StringBuilder message = new StringBuilder("Could not connect to provided CM Synergy session: "); message.append(sessionName).append(" with session file:").append(sessionFile.getAbsolutePath()); LOG.error(message.toString(), e); throw new OperationFailedException(message.toString(), e); } // Create the projectFourPartName needed for projects with instance // other than 1 to reconfigure properly projectFourPartName = projectSpec + ":project:" + projectInstance; LOG.info("Checking for modifications between " + lastBuild.toString() + " and " + now.toString()); // If we were asked to update the folders, do so if (updateFolders) { refreshReconfigureProperties(); } // Create a list of modifications based upon tasks completed // since the last build. numObjects = 0; numTasks = 0; final List<Modification> modifications = getModifiedTasks(lastBuild); LOG.info("Found " + numObjects + " modified object(s) in " + numTasks + " new task(s)."); // If we were asked to reconfigure the project, do so if (reconfigure && (numObjects > 0)) { reconfigureProject(); } // Pass to the build any relevent properties properties.put("cc.ccm.project", projectFourPartName); properties.put("cc.ccm.dateformat", ccmDateFormat); String sessionID = cmd.getVariable(CCM_SESSION_VAR); if (sessionID != null) { properties.put("cc.ccm.session", sessionID); } if (numObjects > 0) { properties.put(property, "true"); } if (!ignoreWorkarea) { properties.put("cc.ccm.workarea", getWorkarea()); } return modifications; } /** * Update the folders within the given project's reconfigure properties. */ private void refreshReconfigureProperties() { LOG.debug("Refreshing reconfigure properties for project " + projectFourPartName + "."); // Construct the CM Synergy command cmd.clearArgs(); cmd.createArgument("reconfigure_properties"); if (recurse) { cmd.createArgument("-recurse"); } cmd.createArguments("-refresh", projectFourPartName); try { cmd.execute(); cmd.assertExitCode(0); } catch (Exception e) { String message = "Could not refresh reconfigure properties for project \"" + projectFourPartName + "\"."; LOG.error(message, e); throw new OperationFailedException(message, e); } } /** * Get a list of all tasks which are contained in all folders in the * reconfigure properties of the specified project and were completed after * the last build. If useBindTime is <code>true</code> not the completion time of * the task is considered but the time the task came into the folder. * * @param lastBuild date of last build * @return A list of <code>CMSynergyModifications</code> which represent * the new tasks */ private List<Modification> getModifiedTasks(final Date lastBuild) { // The format used for converting Java dates into CM Synergy dates // Note that the format used to submit commands differs from the // format used in the results of that command!?! final SimpleDateFormat toCcmDate = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss", locale); boolean isBaselineBasedProject = false; // Determine if the project has a System Testing purpose cmd.clearArgs(); cmd.createArgument("prop"); cmd.createArgument("-p"); cmd.createArgument(projectFourPartName); cmd.createArgument("-f"); cmd.createArgument("%purpose"); try { cmd.execute(); } catch (Exception e) { String message = "Could not query for new tasks."; LOG.error(message, e); throw new OperationFailedException(message, e); } // Determine if the project is either of a System Testing or // Insulated Development purpose. If it is, set the appropriate flag if (cmd.getStdoutAsString().startsWith("System Testing")) { isBaselineBasedProject = true; } else if (cmd.getStdoutAsString().startsWith("Insulated Development")) { isBaselineBasedProject = true; } // get the release of the project final String release = getProjectRelease(projectFourPartName); final String latestBaseline = getLatestBaseline(release); // Construct the CM Synergy command cmd.clearArgs(); cmd.createArgument("query"); cmd.createArgument("-u"); // Set up the output format cmd.createArgument("-f"); cmd.createArgument("%displayname" + CCM_ATTR_DELIMITER + // 0 "%release" + CCM_ATTR_DELIMITER + // 1 "%owner" + CCM_ATTR_DELIMITER + // 2 "%completion_date" + CCM_ATTR_DELIMITER + // 3 "%task_synopsis" + CCM_END_OBJECT); // 4 // Construct the query string if ((useBindTime & (!isBaselineBasedProject))) { cmd.createArgument( "is_task_in_folder_of(is_folder_in_rp_of('" + projectFourPartName + "'), '>', time('" + toCcmDate.format(lastBuild) + "'))"); } else if ((!isBaselineBasedProject)) { cmd.createArgument( "is_task_in_folder_of(is_folder_in_rp_of('" + projectFourPartName + "')) and completion_date>time('" + toCcmDate.format(lastBuild) + "')"); } else if ((useBindTime & isBaselineBasedProject)) { cmd.createArgument( "is_task_in_baseline_of('" + latestBaseline + "'), '>', time('" + toCcmDate.format(lastBuild) + "') or is_task_in_folder_of(is_folder_in_rp_of('" + projectFourPartName + "'), '>', time('" + toCcmDate.format(lastBuild) + "'))"); } else { cmd.createArgument( "is_task_in_baseline_of('" + latestBaseline + "') and completion_date>time('" + toCcmDate.format(lastBuild) + "') or is_task_in_folder_of(is_folder_in_rp_of('" + projectFourPartName + "')) and completion_date>time('" + toCcmDate.format(lastBuild) + "')"); } // Execute the command try { cmd.execute(); } catch (Exception e) { String message = "Could not query for new tasks."; LOG.error(message, e); throw new OperationFailedException(message, e); } // create a modification list with discovered tasks final List<Modification> modificationList = new ArrayList<Modification>(); for (final String entry : format(cmd.getStdoutAsList())) { numTasks++; final String[] attributes = tokeniseEntry(entry, 5); if (attributes == null) { LOG.warn("Could not determine attributes for at least one " + "discovered task! The modification set is suspect."); continue; } final CMSynergyModification mod = new CMSynergyModification(); mod.taskNumber = attributes[0]; mod.revision = attributes[1]; mod.userName = attributes[2]; mod.modifiedTime = getDateFromSynergy(attributes[3]); mod.comment = attributes[4]; // Populate the included files by quering for objects in the entry getModifiedObjects(mod); // Find any Change Requests with which the entry is associated getAssociatedCRs(mod); // Add the modification to the list modificationList.add(mod); } return modificationList; } /** * Get the latest baseline name for a given release value * @param release * The release value for which to find the latest baseline * @return The name of the latest Synergy baseline for the supplied release value */ private String getLatestBaseline(String release) { // Construct the CM Synergy command cmd.clearArgs(); cmd.createArgument("query"); cmd.createArgument("-u"); cmd.createArgument("-ns"); // Set up the output format cmd.createArgument("-f"); cmd.createArgument("%objectname"); // Construct the query string cmd.createArgument("status='published_baseline' and release='" + release + "'"); // Execute the command try { cmd.execute(); } catch (Exception e) { String message = "Could not query for latest baseline"; LOG.error(message, e); throw new OperationFailedException(message, e); } String baselineString = cmd.getStdoutAsString(); String[] baselineList = baselineString.split("\r\n|\r|\n"); return baselineList[baselineList.length - 1].trim(); } /** * Get the Synergy Release value for a given project * @param projectFourPartName * The project whose release you wish to obtain * @return The release value for the given project */ private String getProjectRelease(String projectFourPartName) { cmd.clearArgs(); cmd.createArgument("prop"); cmd.createArgument("-p"); cmd.createArgument(projectFourPartName); cmd.createArgument("-f"); cmd.createArgument("%release"); try { cmd.execute(); } catch (Exception e) { String message = "Could not get project release"; LOG.error(message, e); throw new OperationFailedException(message, e); } String[] release = cmd.getStdoutAsString().split("\r\n|\r|\n"); return release[0].trim(); } /** * Split the results of a CM Synergy query into individual tokens. This * method was added for compatibility with the 1.3 JRE. * * @param line * The line to be tokenised. * @param maxTokens * The maximum number of tokens in the line * * @return The tokens found */ private String[] tokeniseEntry(String line, int maxTokens) { int minTokens = maxTokens - 1; // comment may be absent. String[] tokens = new String[maxTokens]; Arrays.fill(tokens, ""); int tokenIndex = 0; for (int oldIndex = 0, index = line.indexOf(CCM_ATTR_DELIMITER, 0); true; oldIndex = index + CCM_ATTR_DELIMITER.length(), index = line.indexOf(CCM_ATTR_DELIMITER, oldIndex), tokenIndex++) { if (tokenIndex > maxTokens) { LOG.debug("Too many tokens; skipping entry"); return null; } if (index == -1) { tokens[tokenIndex] = line.substring(oldIndex); break; } tokens[tokenIndex] = line.substring(oldIndex, index); } if (tokenIndex < minTokens) { LOG.debug("Not enough tokens; skipping entry"); return null; } return tokens; } /** * Populate the object list of a Modification by querying for objects * associated with the task. * @param mod the modification object to be populated */ private void getModifiedObjects(CMSynergyModification mod) { // Construct the CM Synergy command cmd.clearArgs(); cmd.createArgument("task"); cmd.createArguments("-show", "objects"); // Set up the output format cmd.createArgument("-f"); cmd.createArgument("%name" + CCM_ATTR_DELIMITER + // 0 "%version" + CCM_ATTR_DELIMITER + // 1 "%type" + CCM_ATTR_DELIMITER + // 2 "%instance" + CCM_ATTR_DELIMITER + // 3 "%project" + CCM_ATTR_DELIMITER + // 4 "%comment" + CCM_END_OBJECT); // 5 // Construct the query string cmd.createArgument(mod.taskNumber); // Execute the command try { cmd.execute(); } catch (Exception e) { LOG.warn("Could not query for objects in task \"" + mod.taskNumber + "\". The modification list will be incomplete!", e); } // Populate the modification with the object data from the task for (final String object : format(cmd.getStdoutAsList())) { numObjects++; final String[] attributes = tokeniseEntry(object, 6); if (attributes == null) { LOG.warn("Could not determine attributes for object associated " + "with task \"" + mod.revision + "\"."); continue; } // Add each object to the CMSynergyModification mod.createModifiedObject(attributes[0], attributes[1], attributes[2], attributes[3], attributes[4], attributes[5]); } } /** * Queries the CM Synergy repository to find any Change Requests with which * a task is associated. If the Change Synergy URL and database were * provided, we will add HTML based links to those CRs. * * @param mod * The modification object */ private void getAssociatedCRs(CMSynergyModification mod) { // Construct the CM Synergy command cmd.clearArgs(); cmd.createArgument("query"); cmd.createArgument("-u"); // Set up the output format cmd.createArguments("-f", "%displayname" + CCM_ATTR_DELIMITER + "%change_request_synopsis"); // Construct the query string cmd.createArgument( "cvtype='problem' and has_associated_task('task" + mod.taskNumber + ccmDelimiter + "1:task:probtrac')"); // Execute the command try { cmd.execute(); } catch (Exception e) { LOG.warn("Could not query for associated CRs. The modification list " + "may be incomplete!", e); } // Add the Change Request(s) to the modification final List<String> crList = cmd.getStdoutAsList(); if (crList != null) { for (final String aCrList : crList) { final String[] crDetails = tokeniseEntry(aCrList, 2); final String crNum = crDetails[0]; final String crSynopsis = crDetails[1].trim(); final CMSynergyModification.ChangeRequest cr = mod.createChangeRequest(crNum, crSynopsis); if (changeSynergyURL != null && ccmDb != null) { final StringBuilder href = new StringBuilder(changeSynergyURL); href.append("/servlet/com.continuus.webpt.servlet.PTweb?"); href.append("ACTION_FLAG=frameset_form&TEMPLATE_FLAG=ProblemReportView&database="); href.append(ccmDb); href.append("&role=User&problem_number="); href.append(crNum); cr.href = href.toString(); } } } } /** * Determine the work area location for the specified project. * * @return The work area location */ private String getWorkarea() { String defaultWorkarea = "."; // Get the literal workarea from Synergy cmd.clearArgs(); cmd.createArgument("attribute"); cmd.createArguments("-show", "wa_path"); cmd.createArguments("-project", projectFourPartName); try { cmd.execute(); cmd.assertExitCode(0); } catch (Exception e) { LOG.warn("Could not determine the workarea location for project \"" + projectFourPartName + "\".", e); return defaultWorkarea; } // The command will return the literal work area, but what we are // really interested in is the top level directory within that work // area. File workareaPath = new File(cmd.getStdoutAsString().trim()); if (!workareaPath.isDirectory()) { LOG.warn("The workarea reported by Synergy does not exist or is not accessible by this session - \"" + workareaPath.toString() + "\"."); return defaultWorkarea; } String[] dirs = workareaPath.list(); if (dirs.length != 1) { LOG.warn("The workarea reported by Synergy is invalid - \"" + workareaPath.toString() + "\"."); return defaultWorkarea; } // Found it! return workareaPath.getAbsolutePath() + File.separator + dirs[0]; } /** * Reconfigure the project */ private void reconfigureProject() { LOG.debug("Reconfiguring project " + projectFourPartName + "."); // Construct the CM Synergy command cmd.clearArgs(); cmd.createArgument("reconfigure"); if (recurse) { cmd.createArgument("-recurse"); } cmd.createArguments("-project", projectFourPartName); try { cmd.execute(); cmd.assertExitCode(0); } catch (Exception e) { String message = "Could not reconfigure project \"" + projectFourPartName + "\"."; LOG.error(message, e); throw new OperationFailedException(message, e); } } /** * Format the output of a CM Synergy query by removing newlines introduced * by comments. * * @param in * The <code>List</code> to be formated * @return The formated <code>List</code> */ private List<String> format(final List<String> in) { // Concatenate output lines until we hit the end of object delimiter. List<String> out = new ArrayList<String>(); Iterator it = in.iterator(); StringBuilder buff = new StringBuilder(); while (it.hasNext()) { buff.append((String) it.next()); int index = buff.toString().lastIndexOf(CCM_END_OBJECT); if (index > -1) { buff.delete(index, buff.length()); out.add(buff.toString()); buff = new StringBuilder(); } } return out; } /** * Parse a CM Synergy date string into a Java <code>Date</code>. If the * string cannot be parsed, a warning is written to the log, and the current * date is returned. * * @param dateString * the date string to parse * @return The date * * @see #setCcmDateFormat(String) */ private Date getDateFromSynergy(String dateString) { SimpleDateFormat fromCcmDate = new SimpleDateFormat(ccmDateFormat, locale); Date date; try { date = fromCcmDate.parse(dateString); } catch (ParseException e) { LOG.warn("Could not parse CM Synergy date \"" + dateString + "\" into Java Date using format \"" + ccmDateFormat + "\".", e); date = new Date(); } return date; } /** * Given a CM Synergy session name, looks up the corresponding session ID. * * @param sessionName * The CM Synergy session name * @param sessionFile * The session map file * @return The session ID. * @throws CruiseControlException if something bad happens. */ private static String getSessionID(String sessionName, File sessionFile) throws CruiseControlException { // If no session file was provided, try to use the default if (sessionFile == null) { sessionFile = new File(CCM_SESSION_FILE); } // Load the persisted session information from file Properties sessionProperties; try { sessionProperties = Util.loadPropertiesFromFile(sessionFile); } catch (IOException e) { throw new CruiseControlException(e); } // Look up and return the full session ID return sessionProperties.getProperty(sessionName); } /** * Creates a <code>ManagedCommandline</code> configured to run CM Synergy * commands. * * @param ccmExe * Full path of the CM Synergy command line client (or * <code>null</code> to use the default). * @param sessionName * The name of the session as stored in the map file (or * <code>null</code> to use the default session). * @param sessionFile * The CM Synergy session map file (or <code>null</code> to use * the default). * @return A configured <code>ManagedCommandline</code> */ @SkipDoc public static ManagedCommandline createCcmCommand(String ccmExe, String sessionName, File sessionFile) { // If no executable name was provided, use the default if (ccmExe == null) { ccmExe = CCM_EXE; } // Attempt to get the appropriate CM Synergy session String sessionID = null; if (sessionName != null) { try { sessionID = getSessionID(sessionName, sessionFile); if (sessionID == null) { LOG.error("Could not find a session ID for CM Synergy session named \"" + sessionName + "\". Attempting to use the default (current) session."); } } catch (CruiseControlException e) { LOG.error("Failed to look up CM Synergy session named \"" + sessionName + "\". Attempting to use the default (current) session.", e); } } // Create a managed command line ManagedCommandline command = new ManagedCommandline(ccmExe); // If we were able to find a CM Synergy session ID, use it if (sessionID != null) { command.setVariable(CCM_SESSION_VAR, sessionID); } return command; } class OperationFailedException extends RuntimeException { public OperationFailedException(String message, Exception e) { super(message, e); } } }