001: /********************************************************************************
002: * CruiseControl, a Continuous Integration Toolkit
003: * Copyright (c) 2001, ThoughtWorks, Inc.
004: * 200 E. Randolph, 25th Floor
005: * Chicago, IL 60601 USA
006: * All rights reserved.
007: *
008: * Redistribution and use in source and binary forms, with or without
009: * modification, are permitted provided that the following conditions
010: * are met:
011: *
012: * + Redistributions of source code must retain the above copyright
013: * notice, this list of conditions and the following disclaimer.
014: *
015: * + Redistributions in binary form must reproduce the above
016: * copyright notice, this list of conditions and the following
017: * disclaimer in the documentation and/or other materials provided
018: * with the distribution.
019: *
020: * + Neither the name of ThoughtWorks, Inc., CruiseControl, nor the
021: * names of its contributors may be used to endorse or promote
022: * products derived from this software without specific prior
023: * written permission.
024: *
025: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
026: * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
027: * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
028: * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR
029: * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
030: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
031: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
032: * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
033: * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
034: * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
035: * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
036: ********************************************************************************/package net.sourceforge.cruisecontrol.sourcecontrols;
037:
038: import java.io.BufferedReader;
039: import java.io.File;
040: import java.io.IOException;
041: import java.io.InputStream;
042: import java.io.InputStreamReader;
043: import java.text.ParseException;
044: import java.text.SimpleDateFormat;
045: import java.util.ArrayList;
046: import java.util.Arrays;
047: import java.util.Date;
048: import java.util.Hashtable;
049: import java.util.List;
050: import java.util.Map;
051: import java.util.StringTokenizer;
052: import java.util.Vector;
053:
054: import net.sourceforge.cruisecontrol.CruiseControlException;
055: import net.sourceforge.cruisecontrol.SourceControl;
056: import net.sourceforge.cruisecontrol.util.DiscardConsumer;
057: import net.sourceforge.cruisecontrol.util.StreamPumper;
058: import net.sourceforge.cruisecontrol.util.ValidationHelper;
059: import net.sourceforge.cruisecontrol.util.IO;
060: import net.sourceforge.cruisecontrol.util.StreamLogger;
061:
062: import org.apache.log4j.Logger;
063:
064: /**
065: * This class implements the SourceControlElement methods for a Clear Case
066: * repository.
067: *
068: * @author Thomas Leseney
069: * @author <a href="mailto:jcyip@thoughtworks.com">Jason Yip</a>
070: * @author Eric Lefevre
071: * @author Ralf Krakowski
072: */
073: public class ClearCase implements SourceControl {
074: private static final int DEFAULT = 0;
075: private static final int DISABLED = 1;
076: private static final int ENABLED = 2;
077:
078: private static final Logger LOG = Logger.getLogger(ClearCase.class);
079:
080: private SourceControlProperties properties = new SourceControlProperties();
081:
082: /**
083: * The path of the clear case view
084: */
085: private String viewPath;
086:
087: /**
088: * The branch to check for modifications
089: */
090: private String branch;
091: private int recursive = DEFAULT; // default is true
092: private int all = DEFAULT; // default is false
093:
094: /**
095: * Date format required by commands passed to Clear Case
096: */
097: private final SimpleDateFormat inDateFormatter = new SimpleDateFormat(
098: "dd-MMMM-yyyy.HH:mm:ss");
099:
100: /**
101: * Date format returned in the output of Clear Case commands.
102: */
103: private final SimpleDateFormat outDateFormatter = new SimpleDateFormat(
104: "yyyyMMdd.HHmmss");
105:
106: /**
107: * Unlikely combination of characters to separate fields in a ClearCase query
108: */
109: static final String DELIMITER = "#~#";
110:
111: /**
112: * Even more unlikely combination of characters to indicate end of one line in query.
113: * Carriage return (\n) can be used in comments and so is not available to us.
114: */
115: static final String END_OF_STRING_DELIMITER = "@#@#@#@#@#@#@#@#@#@#@#@";
116:
117: /**
118: * Sets the local working copy to use when making queries.
119: *
120: * @param path
121: */
122: public void setViewpath(String path) {
123: //_viewPath = getAntTask().getProject().resolveFile(path).getAbsolutePath();
124: viewPath = new File(path).getAbsolutePath();
125: }
126:
127: /**
128: * Sets the branch that we're concerned about checking files into.
129: *
130: * @param branch
131: */
132: public void setBranch(String branch) {
133: this .branch = branch;
134: }
135:
136: /**
137: * Set whether to check against sub-folders in the view path
138: */
139: public void setRecursive(boolean recursive) {
140: this .recursive = recursive ? ENABLED : DISABLED;
141: }
142:
143: /**
144: * Set when checking the entire view path.
145: * <p/>
146: * When checking the entire view path this option invokes 'lshistory -all'
147: * instead of 'lshistory -recursive', which is much faster.
148: * <p/>
149: * This option is mutually exclusive with the recursive property.
150: * <p/>
151: * Note that 'all' does not use your view's config-spec rules. It behaves
152: * like having a single line config-spec that selects just ELEMENT * /<branch>/LATEST
153: * (i.e. 'lshistory -all' results that contain @@ are discarded). This differs from
154: * 'recurse', which only shows items selected by your current view.
155: */
156: public void setAll(boolean all) {
157: this .all = all ? ENABLED : DISABLED;
158:
159: if (this .recursive == DEFAULT && all) {
160: this .recursive = DISABLED;
161: }
162: }
163:
164: public void setProperty(String property) {
165: properties.assignPropertyName(property);
166: }
167:
168: public Map getProperties() {
169: return properties.getPropertiesAndReset();
170: }
171:
172: public void validate() throws CruiseControlException {
173: ValidationHelper.assertIsSet(viewPath, "viewpath", this
174: .getClass());
175: if (recursive == ENABLED && all == ENABLED) {
176: ValidationHelper
177: .fail("'recursive' and 'all' are mutually exclusive attributes for ClearCase");
178: }
179: }
180:
181: /**
182: * Returns an {@link java.util.List List} of {@link ClearCaseModification}
183: * detailing all the changes between now and the last build.
184: *
185: * @param lastBuild the last build time
186: * @param now time now, or time to check, NOT USED
187: * @return the list of modifications, an empty (not null) list if no
188: * modifications.
189: */
190: public List getModifications(Date lastBuild, Date now) {
191: String lastBuildDate = inDateFormatter.format(lastBuild);
192: String nowDate = inDateFormatter.format(now);
193: properties.put("clearcaselastbuild", lastBuildDate);
194: properties.put("clearcasenow", nowDate);
195:
196: /*
197: * let's try a different clearcase command--this one just takes
198: * waaaaaaaay too long.
199: * String command = "cleartool find " + _viewPath +
200: * " -type f -exec \"cleartool lshistory" +
201: * " -since " + lastBuildDate;
202: * if(_branch != null)
203: * command += " -branch " + _branch;
204: * command += " -nco" + // exclude check out events
205: * " -fmt \\\" %u;%Nd;%n;%o \\n \\\" \\\"%CLEARCASE_XPN%\\\" \"";
206: */
207: String command = "cleartool lshistory";
208:
209: if (branch != null) {
210: command += " -branch " + branch;
211: }
212:
213: if (recursive == DEFAULT || recursive == ENABLED) {
214: command += " -r";
215: } else if (all == ENABLED) {
216: command += " -all";
217: }
218:
219: command += " -nco -since " + lastBuildDate;
220: command += " -fmt %u" + DELIMITER + "%Nd" + DELIMITER + "%En"
221: + DELIMITER + "%Vn" + DELIMITER + "%o" + DELIMITER
222: + "!%l" + DELIMITER + "!%a" + DELIMITER + "%Nc"
223: + END_OF_STRING_DELIMITER + "\\n";
224:
225: File root = new File(viewPath);
226:
227: LOG.info("ClearCase: getting modifications for " + viewPath);
228:
229: LOG.debug("Command to execute : " + command);
230: List modifications = null;
231: try {
232: Process p = Runtime.getRuntime().exec(command, null, root);
233: p.getOutputStream().close();
234:
235: Thread stderr = logErrorStream(p);
236:
237: InputStream input = p.getInputStream();
238: modifications = parseStream(input);
239:
240: getRidOfLeftoverData(input);
241: p.waitFor();
242: stderr.join();
243: IO.close(p);
244: } catch (Exception e) {
245: LOG
246: .error(
247: "Error in executing the Clear Case command : ",
248: e);
249: }
250:
251: if (modifications == null) {
252: modifications = new ArrayList();
253: }
254:
255: return modifications;
256: }
257:
258: private Thread logErrorStream(Process process) {
259: Thread stderr = new Thread(StreamLogger.getWarnPumper(LOG,
260: process));
261: stderr.start();
262: return stderr;
263: }
264:
265: private void getRidOfLeftoverData(InputStream stream) {
266: new StreamPumper(stream, new DiscardConsumer()).run();
267: }
268:
269: /**
270: * Parses the input stream to construct the modifications list.
271: * Package-private to make it available to the unit test.
272: *
273: * @param input the stream to parse
274: * @return a list of modification elements
275: * @throws IOException
276: */
277: List parseStream(InputStream input) throws IOException {
278: ArrayList modifications = new ArrayList();
279: BufferedReader reader = new BufferedReader(
280: new InputStreamReader(input));
281: String ls = System.getProperty("line.separator");
282:
283: String line;
284: String lines = "";
285:
286: while ((line = reader.readLine()) != null) {
287: if (!lines.equals("")) {
288: lines += ls;
289: }
290: lines += line;
291: ClearCaseModification mod = null;
292: if (lines.indexOf(END_OF_STRING_DELIMITER) > -1) {
293: mod = parseEntry(lines.substring(0, lines
294: .indexOf(END_OF_STRING_DELIMITER)));
295: lines = "";
296: }
297: if (mod != null) {
298: modifications.add(mod);
299: }
300: }
301: return modifications;
302: }
303:
304: /**
305: * Parses a single line from the reader. Each line contains a single revision
306: * with the format : <br>
307: * username#~#date_of_revision#~#element_name#~#operation_type#~#comments <br>
308: *
309: * @param line the line to parse
310: * @return a modification element corresponding to the given line
311: */
312: private ClearCaseModification parseEntry(String line) {
313: LOG.debug("parsing entry: " + line);
314: String[] tokens = tokeniseEntry(line);
315: if (tokens == null) {
316: return null;
317: }
318: String username = tokens[0].trim();
319:
320: String timeStamp = tokens[1].trim();
321: String elementName = tokens[2].trim();
322: String version = tokens[3].trim();
323: String operationType = tokens[4].trim();
324:
325: String labelList = tokens[5].substring(1).trim();
326: Vector labels = extractLabelsList(labelList);
327:
328: String attributeList = tokens[6].substring(1).trim();
329: Hashtable attributes = extractAttributesMap(attributeList);
330:
331: String comment = tokens[7].trim();
332:
333: // A branch event shouldn't trigger a build
334: if (operationType.equals("mkbranch")
335: || operationType.equals("rmbranch")) {
336: return null;
337: }
338:
339: // Element names that contain @@ are discarded (see setAll(boolean))
340: if (elementName.indexOf("@@") >= 0) {
341: return null;
342: }
343:
344: ClearCaseModification mod = new ClearCaseModification();
345:
346: mod.userName = username;
347: mod.revision = version;
348:
349: String folderName, fileName;
350: int sep = elementName.lastIndexOf(File.separator);
351: if (sep > -1) {
352: folderName = elementName.substring(0, sep);
353: fileName = elementName.substring(sep + 1);
354: } else {
355: folderName = null;
356: fileName = elementName;
357: }
358: ClearCaseModification.ModifiedFile modfile = mod
359: .createModifiedFile(fileName, folderName);
360:
361: try {
362: mod.modifiedTime = outDateFormatter.parse(timeStamp);
363: } catch (ParseException e) {
364: mod.modifiedTime = null;
365: }
366:
367: modfile.action = operationType;
368: modfile.revision = version;
369:
370: mod.type = "clearcase";
371: mod.labels = labels;
372: mod.attributes = attributes;
373:
374: mod.comment = comment;
375: properties.modificationFound();
376:
377: // TODO: check if operation type is a delete
378:
379: return mod;
380: }
381:
382: private String[] tokeniseEntry(String line) {
383: int maxTokens = 8;
384: int minTokens = maxTokens - 1; // comment may be absent.
385: String[] tokens = new String[maxTokens];
386: Arrays.fill(tokens, "");
387: int tokenIndex = 0;
388: for (int oldIndex = 0, index = line.indexOf(DELIMITER, 0); true; oldIndex = index
389: + DELIMITER.length(), index = line.indexOf(DELIMITER,
390: oldIndex), tokenIndex++) {
391: if (tokenIndex > maxTokens) {
392: LOG.debug("Too many tokens; skipping entry");
393: return null;
394: }
395: if (index == -1) {
396: tokens[tokenIndex] = line.substring(oldIndex);
397: break;
398: } else {
399: tokens[tokenIndex] = line.substring(oldIndex, index);
400: }
401: }
402: if (tokenIndex < minTokens) {
403: LOG.debug("Not enough tokens; skipping entry");
404: return null;
405: }
406: return tokens;
407: }
408:
409: /**
410: * @param attributeList
411: * @return parsed list
412: */
413: private Hashtable extractAttributesMap(String attributeList) {
414: Hashtable attributes = null;
415: if (attributeList.length() > 0) {
416: attributes = new Hashtable();
417: StringTokenizer attrST = new StringTokenizer(attributeList,
418: "(), ");
419: while (attrST.hasMoreTokens()) {
420: String attr = attrST.nextToken();
421: int idx = attr.indexOf('=');
422: if (idx > 0) {
423: String attrName = attr.substring(0, idx);
424: String attrValue = attr.substring(idx + 1);
425: if (attrValue.startsWith("\"")) {
426: attrValue = attrValue.substring(1, attrValue
427: .length() - 1);
428: }
429: attributes.put(attrName, attrValue);
430: }
431: }
432: }
433: return attributes;
434: }
435:
436: /**
437: * @param labelList
438: * @return parsed list
439: */
440: private Vector extractLabelsList(String labelList) {
441: Vector labels = null;
442: if (labelList.length() > 0) {
443: labels = new Vector();
444: StringTokenizer labelST = new StringTokenizer(labelList,
445: "(), ");
446: while (labelST.hasMoreTokens()) {
447: labels.add(labelST.nextToken().trim());
448: }
449: }
450: return labels;
451: }
452: }
|