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 modification, are permitted provided that the
009: * following conditions are met:
010: *
011: * Redistributions of source code must retain the above copyright notice, this list of conditions and the
012: * following disclaimer.
013: *
014: * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the
015: * following disclaimer in the documentation and/or other materials provided with the distribution.
016: *
017: * Neither the name of ThoughtWorks, Inc., CruiseControl, nor the names of its contributors may be used to endorse
018: * or promote products derived from this software without specific prior written permission.
019: *
020: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
021: * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
022: * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
023: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
024: * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
025: * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
026: * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
027: **********************************************************************************************************************/package net.sourceforge.cruisecontrol.sourcecontrols;
028:
029: import java.io.BufferedReader;
030: import java.io.File;
031: import java.io.FileReader;
032: import java.io.IOException;
033: import java.io.InputStream;
034: import java.text.ParseException;
035: import java.text.SimpleDateFormat;
036: import java.util.ArrayList;
037: import java.util.Collections;
038: import java.util.Date;
039: import java.util.Iterator;
040: import java.util.List;
041: import java.util.Locale;
042: import java.util.Map;
043:
044: import net.sourceforge.cruisecontrol.CruiseControlException;
045: import net.sourceforge.cruisecontrol.Modification;
046: import net.sourceforge.cruisecontrol.SourceControl;
047: import net.sourceforge.cruisecontrol.util.Commandline;
048: import net.sourceforge.cruisecontrol.util.ValidationHelper;
049: import net.sourceforge.cruisecontrol.util.IO;
050: import net.sourceforge.cruisecontrol.util.StreamLogger;
051:
052: import org.apache.log4j.Logger;
053:
054: /**
055: * This class handles all VSS-related aspects of determining the modifications since the last good build.
056: *
057: * @author <a href="mailto:alden@thoughtworks.com">alden almagro</a>
058: * @author Eli Tucker
059: * @author <a href="mailto:jcyip@thoughtworks.com">Jason Yip</a>
060: * @author Arun Aggarwal
061: */
062: public class Vss implements SourceControl {
063:
064: private static final Logger LOG = Logger.getLogger(Vss.class);
065:
066: private SimpleDateFormat vssDateTimeFormat;
067: private String ssDir;
068: private String vssPath;
069: private String serverPath;
070: private String login;
071: private String dateFormat = "MM/dd/yy";
072: private String timeFormat = "hh:mma";
073: private SourceControlProperties properties = new SourceControlProperties();
074:
075: public Vss() {
076: constructVssDateTimeFormat();
077: }
078:
079: /**
080: * Set the project to get history from
081: *
082: * @param vsspath
083: */
084: public void setVsspath(String vsspath) {
085: this .vssPath = "$" + vsspath;
086: }
087:
088: /**
089: * Set the path to the ss executable
090: *
091: * @param ssdir
092: */
093: public void setSsDir(String ssdir) {
094: this .ssDir = ssdir;
095: }
096:
097: /**
098: * Set the path to the directory containing the srcsafe.ini file.
099: *
100: * @param dirWithSrcsafeIni
101: */
102: public void setServerPath(String dirWithSrcsafeIni) {
103: serverPath = dirWithSrcsafeIni;
104: }
105:
106: /**
107: * Login for vss
108: *
109: * @param usernameCommaPassword
110: */
111: public void setLogin(String usernameCommaPassword) {
112: login = usernameCommaPassword;
113: }
114:
115: /**
116: * Choose a property to be set if the project has modifications if we have a change that only requires repackaging,
117: * i.e. jsp, we don't need to recompile everything, just rejar.
118: *
119: * @param propertyName
120: */
121: public void setProperty(String propertyName) {
122: properties.assignPropertyName(propertyName);
123: }
124:
125: /**
126: * Choose a property to be set if the project has deletions
127: *
128: * @param propertyName
129: */
130: public void setPropertyOnDelete(String propertyName) {
131: properties.assignPropertyOnDeleteName(propertyName);
132: }
133:
134: /**
135: * Sets the date format to use for querying VSS and processing reports. The default date format is
136: * <code>MM/dd/yy</code> . If your computer is set to a different region, you may wish to use a format such as
137: * <code>dd/MM/yy</code> .
138: *
139: * @see java.text.SimpleDateFormat
140: */
141: public void setDateFormat(String format) {
142: dateFormat = format;
143: constructVssDateTimeFormat();
144: }
145:
146: /**
147: * Sets the time format to use for querying VSS and processing reports. The default time format is
148: * <code>hh:mma</code> . If your computer is set to a different region, you may wish to use a format such as
149: * <code>HH:mm</code> .
150: *
151: * @see java.text.SimpleDateFormat
152: */
153: public void setTimeFormat(String format) {
154: timeFormat = format;
155: constructVssDateTimeFormat();
156: }
157:
158: public Map getProperties() {
159: return properties.getPropertiesAndReset();
160: }
161:
162: public void validate() throws CruiseControlException {
163: ValidationHelper.assertIsSet(vssPath, "vsspath", this
164: .getClass());
165: ValidationHelper.assertIsSet(login, "login", this .getClass());
166: }
167:
168: /**
169: * Calls "ss history [dir] -R -Vd[now]~[lastBuild] -Y[login] -I-N -O[tempFileName]" Results written to a file since
170: * VSS will start wrapping lines if read directly from the stream.
171: *
172: * @param lastBuild
173: * @param now
174: * @return List of modifications
175: */
176: public List getModifications(Date lastBuild, Date now) {
177: List modifications = new ArrayList();
178:
179: Process p = null;
180: try {
181: LOG.info("Getting modifications for " + vssPath);
182: p = Runtime.getRuntime().exec(
183: getCommandLine(lastBuild, now),
184: VSSHelper.loadVSSEnvironment(serverPath));
185: p.getOutputStream().close();
186: Thread stderr = logErrorStream(p.getErrorStream());
187:
188: p.waitFor();
189: stderr.join();
190:
191: parseTempFile(modifications);
192: } catch (Exception e) {
193: // TODO: Revisit this when ThreadQueue is more stable. Would prefer throwing a RuntimeException.
194: LOG
195: .error(
196: "Problem occurred while attempting to get VSS modifications. Returning empty modifications.",
197: e);
198: return Collections.EMPTY_LIST;
199: } finally {
200: IO.close(p);
201: }
202:
203: if (modifications.size() > 0) {
204: properties.modificationFound();
205: }
206:
207: return modifications;
208: }
209:
210: private Thread logErrorStream(InputStream is) {
211: Thread stderr = new Thread(StreamLogger.getWarnPumper(LOG, is));
212: stderr.start();
213: return stderr;
214: }
215:
216: void parseTempFile(List modifications) throws IOException,
217: CruiseControlException {
218: File tempFile = getTempFile();
219: if (!getTempFile().isFile()) {
220: throw new CruiseControlException(
221: "vss failed to create output file "
222: + tempFile.getPath());
223: }
224:
225: if (LOG.isDebugEnabled()) {
226: logVSSTempFile();
227: }
228:
229: final BufferedReader reader = new BufferedReader(
230: new FileReader(tempFile));
231: try {
232: parseHistoryEntries(modifications, reader);
233: } finally {
234: // need to close the InputStream before delete(), otherwise fails on windows
235: IO.close(reader);
236: }
237: tempFile.delete();
238: }
239:
240: private void logVSSTempFile() throws IOException {
241: BufferedReader reader = new BufferedReader(new FileReader(
242: getTempFile()));
243: String currLine = reader.readLine();
244: LOG.debug(" ");
245: while (currLine != null) {
246: LOG.debug(getTempFile().getName() + ": " + currLine);
247: currLine = reader.readLine();
248: }
249: LOG.debug(" ");
250: reader.close();
251: }
252:
253: private File getTempFile() {
254: return new File(createFileNameFromVssPath());
255: }
256:
257: String createFileNameFromVssPath() {
258: // don't include the leading $
259: String filename = vssPath.substring(1).replace('/', '_')
260: + ".tmp";
261: while (filename.charAt(0) == '_') {
262: filename = filename.substring(1);
263: }
264: return filename;
265: }
266:
267: void parseHistoryEntries(List modifications, BufferedReader reader)
268: throws IOException {
269: String currLine = reader.readLine();
270:
271: while (currLine != null) {
272: if (isRelevantVssEntryHeader(currLine)) {
273: List vssEntry = new ArrayList();
274: vssEntry.add(currLine);
275: currLine = reader.readLine();
276: while (currLine != null
277: && !isRelevantVssEntryHeader(currLine)) {
278: vssEntry.add(currLine);
279: currLine = reader.readLine();
280: }
281: Modification mod = handleEntry(vssEntry);
282: if (mod != null) {
283: modifications.add(mod);
284: }
285: } else {
286: currLine = reader.readLine();
287: }
288: }
289: }
290:
291: /**
292: * Most relevant VSS entry headers will be 5 asterisks, 2 spaces, file name, 2 spaces, 5 asterisks. However, if
293: * adding to the root, there are apparently 17 asterisks, 2 spaces, version name, 2 spaces, 17 asterisks.
294: */
295: private boolean isRelevantVssEntryHeader(String line) {
296: // This can still fail if the entry has a comment containing something of the form '***** some text *****' but
297: // is probably not worth handling at this point. If it does come up, we may need to look at implementing some
298: // form of state machine.
299:
300: return line.matches("\\*+ {2}.+ {2}\\*+");
301: }
302:
303: protected String[] getCommandLine(Date lastBuild, Date now)
304: throws IOException {
305: Commandline commandline = new Commandline();
306: String execCommand = (ssDir != null) ? new File(ssDir, "ss.exe")
307: .getCanonicalPath()
308: : "ss.exe";
309:
310: commandline.setExecutable(execCommand);
311: commandline.createArgument("history");
312: commandline.createArgument(vssPath);
313: commandline.createArgument("-R");
314: commandline.createArgument("-Vd" + formatDateForVSS(now) + "~"
315: + formatDateForVSS(lastBuild));
316: commandline.createArgument("-Y" + login);
317: commandline.createArgument("-I-N");
318: commandline.createArgument("-O" + getTempFile().getName());
319:
320: LOG.info("Command line to execute: " + commandline);
321:
322: return commandline.getCommandline();
323: }
324:
325: /**
326: * Format a date for vss in the format specified by the dateFormat. By default, this is in the form
327: * <code>12/21/2000;8:14A</code> (vss doesn't like the m in am or pm). This format can be changed with
328: * <code>setDateFormat()</code>
329: *
330: * @param date
331: * Date to format.
332: * @return String of date in format that VSS requires.
333: * @see #setDateFormat
334: */
335: private String formatDateForVSS(Date date) {
336: String vssFormattedDate = new SimpleDateFormat(dateFormat + ";"
337: + timeFormat, Locale.US).format(date);
338: if (timeFormat.endsWith("a")) {
339: return vssFormattedDate.substring(0, vssFormattedDate
340: .length() - 1);
341: }
342:
343: return vssFormattedDate;
344: }
345:
346: /**
347: * Parse individual VSS history entry
348: *
349: * @param entry
350: */
351: protected Modification handleEntry(List entry) {
352: LOG.debug("VSS history entry BEGIN");
353: for (Iterator i = entry.iterator(); i.hasNext();) {
354: LOG.debug("entry: " + i.next());
355: }
356: LOG.debug("VSS history entry END");
357:
358: final String labelDelimiter = "**********************";
359: boolean isLabelEntry = labelDelimiter.equals(entry.get(0));
360:
361: if (isLabelEntry) {
362: LOG.debug("this is a label; ignoring this entry");
363: return null;
364: }
365:
366: // but need to adjust for cases where Label: line exists
367: //
368: // ***** DateChooser.java *****
369: // Version 8
370: // Label: "Completely new version!"
371: // User: Arass Date: 10/21/02 Time: 12:48p
372: // Checked in $/code/development/src/org/ets/cbtidg/common/gui
373: // Comment: This is where I add a completely new, but alot nicer
374: // version of the date chooser.
375: // Label comment:
376:
377: int nameAndDateIndex = 2;
378: if (((String) entry.get(0)).startsWith("***************** ")) {
379: nameAndDateIndex = 1;
380: }
381: String nameAndDateLine = (String) entry.get(nameAndDateIndex);
382: if (nameAndDateLine.startsWith("Label:")) {
383: nameAndDateIndex++;
384: nameAndDateLine = (String) entry.get(nameAndDateIndex);
385: LOG.debug("adjusting for the line that starts with Label");
386: }
387:
388: Modification modification = new Modification("vss");
389: modification.userName = parseUser(nameAndDateLine);
390: modification.modifiedTime = parseDate(nameAndDateLine);
391:
392: String folderLine = (String) entry.get(0);
393: int fileIndex = nameAndDateIndex + 1;
394: String fileLine = (String) entry.get(fileIndex);
395: LOG.debug("File line is: " + fileLine);
396:
397: if (fileLine.startsWith("Checked in")) {
398: LOG.debug("this is a checkin");
399: int commentIndex = fileIndex + 1;
400: modification.comment = parseComment(entry, commentIndex);
401: String fileName = folderLine.substring(7, folderLine
402: .indexOf(" *"));
403: String folderName = fileLine.substring(12);
404:
405: Modification.ModifiedFile modfile = modification
406: .createModifiedFile(fileName, folderName);
407: modfile.action = "checkin";
408: } else if (fileLine.endsWith("Created")) {
409: modification.type = "create";
410: LOG.debug("this folder was created");
411: } else {
412: String fileName;
413: String folderName;
414:
415: if (nameAndDateIndex == 1) {
416: folderName = vssPath;
417: } else {
418: folderName = vssPath
419: + "\\"
420: + folderLine.substring(7, folderLine
421: .indexOf(" *"));
422: }
423: int lastSpace = fileLine.lastIndexOf(" ");
424: if (lastSpace != -1) {
425: fileName = fileLine.substring(0, lastSpace);
426: } else {
427: fileName = fileLine;
428: if (fileName.equals("Branched")) {
429: LOG
430: .debug("Branched file, ignoring as branch directory is handled separately");
431: return null;
432: }
433: }
434:
435: Modification.ModifiedFile modfile = modification
436: .createModifiedFile(fileName, folderName);
437:
438: if (fileLine.endsWith("added")) {
439: modfile.action = "add";
440: } else if (fileLine.endsWith("deleted")) {
441: modfile.action = "delete";
442: properties.deletionFound();
443: } else if (fileLine.endsWith("destroyed")) {
444: modfile.action = "destroy";
445: properties.deletionFound();
446: } else if (fileLine.endsWith("recovered")) {
447: modfile.action = "recover";
448: } else if (fileLine.endsWith("shared")) {
449: modfile.action = "share";
450: } else if (fileLine.endsWith("branched")) {
451: modfile.action = "branch";
452: } else if (fileLine.indexOf(" renamed to ") != -1) {
453: modfile.fileName = fileLine;
454: modfile.action = "rename";
455: properties.deletionFound();
456: } else if (fileLine.startsWith("Labeled")) {
457: return null;
458: } else {
459: LOG.warn("Don't know how to handle this line: "
460: + fileLine);
461: return null;
462: }
463: }
464:
465: return modification;
466: }
467:
468: /**
469: * Parse comment from VSS history (could be multi-line)
470: *
471: * @param commentList
472: * @return the comment
473: */
474: private String parseComment(List commentList, int commentIndex) {
475: StringBuffer comment = new StringBuffer();
476: comment.append(commentList.get(commentIndex)).append(" ");
477: for (int i = commentIndex + 1; i < commentList.size(); i++) {
478: comment.append(commentList.get(i)).append(" ");
479: }
480:
481: return comment.toString().trim();
482: }
483:
484: /**
485: * Parse date/time from VSS file history The nameAndDateLine will look like <br>
486: * <code>User: Etucker Date: 6/26/01 Time: 11:53a</code><br>
487: * Sometimes also this<br>
488: * <code>User: Aaggarwa Date: 6/29/:1 Time: 3:40p</code><br>
489: * Note the ":" instead of a "0"
490: *
491: * @param nameAndDateLine
492: * @return Date in form "'Date: 'MM/dd/yy 'Time: 'hh:mma", or a different form based on dateFormat
493: * @see #setDateFormat
494: */
495: public Date parseDate(String nameAndDateLine) {
496: String dateAndTime = nameAndDateLine.substring(nameAndDateLine
497: .indexOf("Date: "));
498:
499: int indexOfColon = dateAndTime.indexOf("/:");
500: if (indexOfColon != -1) {
501: dateAndTime = dateAndTime.substring(0, indexOfColon)
502: + dateAndTime.substring(indexOfColon,
503: indexOfColon + 2).replace(':', '0')
504: + dateAndTime.substring(indexOfColon + 2);
505: }
506:
507: try {
508: Date lastModifiedDate;
509: if (timeFormat.endsWith("a")) {
510: lastModifiedDate = vssDateTimeFormat.parse(dateAndTime
511: .trim()
512: + "m");
513: } else {
514: lastModifiedDate = vssDateTimeFormat.parse(dateAndTime
515: .trim());
516: }
517:
518: return lastModifiedDate;
519: } catch (ParseException pe) {
520: LOG.warn("Could not parse date", pe);
521: return null;
522: }
523: }
524:
525: /**
526: * Parse username from VSS file history
527: *
528: * @param userLine
529: * @return the user name who made the modification
530: */
531: public String parseUser(String userLine) {
532: final int userIndex = "User: ".length();
533:
534: return userLine.substring(userIndex,
535: userLine.indexOf("Date: ") - 1).trim();
536: }
537:
538: /**
539: * Constructs the vssDateTimeFormat based on the dateFormat for this element.
540: *
541: * @see #setDateFormat
542: */
543: private void constructVssDateTimeFormat() {
544: vssDateTimeFormat = new SimpleDateFormat("'Date: '"
545: + dateFormat + " 'Time: '" + timeFormat, Locale.US);
546: }
547:
548: protected SimpleDateFormat getVssDateTimeFormat() {
549: return vssDateTimeFormat;
550: }
551:
552: }
|