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.FileReader;
040: import java.text.ParseException;
041: import java.text.SimpleDateFormat;
042: import java.util.ArrayList;
043: import java.util.Date;
044: import java.util.List;
045: import java.util.Locale;
046: import java.util.Map;
047:
048: import net.sourceforge.cruisecontrol.CruiseControlException;
049: import net.sourceforge.cruisecontrol.Modification;
050: import net.sourceforge.cruisecontrol.SourceControl;
051: import net.sourceforge.cruisecontrol.util.ValidationHelper;
052: import net.sourceforge.cruisecontrol.util.IO;
053:
054: import org.apache.log4j.Logger;
055:
056: /**
057: * This class handles all VSS-related aspects of determining the modifications since the last good build.
058: *
059: * This class uses Source Safe Journal files. Unlike the history files that are generated by executing
060: * <code>ss.exe history</code>, journal files must be setup by the Source Safe administrator before the point that
061: * logging of modifications is to occur.
062: *
063: * This code has been tested against Visual Source Safe v6.0 build 8383.
064: *
065: * @author Eli Tucker
066: * @author <a href="mailto:alden@thoughtworks.com">alden almagro</a>
067: * @author <a href="mailto:jcyip@thoughtworks.com">Jason Yip</a>
068: * @author Arun Aggarwal
069: * @author Jonny Boman
070: * @author <a href="mailto:simon.brandhof@hortis.ch">Simon Brandhof</a>
071: */
072: public class VssJournal implements SourceControl {
073:
074: private static final Logger LOG = Logger
075: .getLogger(VssJournal.class);
076:
077: private String dateFormat;
078: private String timeFormat;
079: private SimpleDateFormat vssDateTimeFormat;
080: private boolean overridenDateFormat = false;
081:
082: private String ssDir = "$/";
083: private String journalFile;
084:
085: private SourceControlProperties properties = new SourceControlProperties();
086:
087: private Date lastBuild;
088:
089: private ArrayList modifications = new ArrayList();
090:
091: public VssJournal() {
092: dateFormat = "MM/dd/yy";
093: timeFormat = "hh:mma";
094: constructVssDateTimeFormat();
095: }
096:
097: /**
098: * Set the project to get history from
099: *
100: */
101: public void setSsDir(String s) {
102: StringBuffer sb = new StringBuffer();
103: if (!s.startsWith("$")) {
104: sb.append("$");
105: }
106: if (s.endsWith("/")) {
107: sb.append(s.substring(0, s.length() - 1));
108: } else {
109: sb.append(s);
110: }
111: this .ssDir = sb.toString();
112: }
113:
114: /**
115: * Full path to journal file. Example: <code>c:/vssdata/journal/journal.txt</code>
116: *
117: * @param journalFile
118: */
119: public void setJournalFile(String journalFile) {
120: this .journalFile = journalFile;
121: }
122:
123: /**
124: * Choose a property to be set if the project has modifications if we have a change that only requires repackaging,
125: * i.e. jsp, we don't need to recompile everything, just rejar.
126: *
127: * @param property
128: */
129: public void setProperty(String property) {
130: properties.assignPropertyName(property);
131: }
132:
133: /**
134: * Set the name of the property to be set if some files were deleted or renamed from VSS on this project.
135: *
136: * @param propertyOnDelete the name of the property to set
137: */
138: public void setPropertyOnDelete(String propertyOnDelete) {
139: properties.assignPropertyOnDeleteName(propertyOnDelete);
140: }
141:
142: /**
143: * Sets the date format to use for parsing VSS journal.
144: *
145: * The default date format is <code>MM/dd/yy</code>. If your VSS server is set to a different region, you may wish
146: * to use a format such as <code>dd/MM/yy</code>.
147: *
148: * @see java.text.SimpleDateFormat
149: */
150: public void setDateFormat(String format) {
151: dateFormat = format;
152: overridenDateFormat = true;
153: constructVssDateTimeFormat();
154: }
155:
156: /**
157: * Sets the time format to use for parsing VSS journal.
158: *
159: * The default time format is <code>hh:mma</code> . If your VSS server is set to a different region, you may wish to
160: * use a format such as <code>HH:mm</code> .
161: *
162: * @see java.text.SimpleDateFormat
163: */
164: public void setTimeFormat(String format) {
165: timeFormat = format;
166: constructVssDateTimeFormat();
167: }
168:
169: private void constructVssDateTimeFormat() {
170: vssDateTimeFormat = new SimpleDateFormat(dateFormat + " "
171: + timeFormat, Locale.US);
172: }
173:
174: /**
175: * Sets the _lastBuild date. Protected so it can be used by tests.
176: */
177: protected void setLastBuildDate(Date lastBuild) {
178: this .lastBuild = lastBuild;
179: }
180:
181: public Map getProperties() {
182: return properties.getPropertiesAndReset();
183: }
184:
185: public void validate() throws CruiseControlException {
186: ValidationHelper.assertIsSet(journalFile, "journalfile", this
187: .getClass());
188: ValidationHelper.assertIsSet(ssDir, "ssdir", this .getClass());
189: }
190:
191: /**
192: * Do the work... I'm writing to a file since VSS will start wrapping lines if I read directly from the stream.
193: */
194: public List getModifications(Date lastBuild, Date now) {
195: this .lastBuild = lastBuild;
196: modifications.clear();
197:
198: try {
199: final BufferedReader br = new BufferedReader(
200: new FileReader(journalFile));
201: try {
202: String s = br.readLine();
203: while (s != null) {
204: ArrayList entry = new ArrayList();
205: entry.add(s);
206: s = br.readLine();
207: while (s != null && !s.equals("")) {
208: entry.add(s);
209: s = br.readLine();
210: }
211: Modification mod = handleEntry(entry);
212: if (mod != null) {
213: modifications.add(mod);
214: }
215:
216: if ("".equals(s)) {
217: s = br.readLine();
218: }
219: }
220: } finally {
221: IO.close(br);
222: }
223: } catch (Exception e) {
224: LOG.warn(e);
225: }
226:
227: if (modifications.size() > 0) {
228: properties.modificationFound();
229: }
230:
231: LOG.info("Found " + modifications.size() + " modified files");
232: return modifications;
233: }
234:
235: /**
236: * Parse individual VSS history entry
237: *
238: * @param historyEntry
239: */
240: protected Modification handleEntry(List historyEntry) {
241: Modification mod = new Modification("vss");
242: String nameAndDateLine = (String) historyEntry.get(2);
243: mod.userName = parseUser(nameAndDateLine);
244: mod.modifiedTime = parseDate(nameAndDateLine);
245:
246: String folderLine = (String) historyEntry.get(0);
247: String fileLine = (String) historyEntry.get(3);
248: boolean setPropertyOnDelete = false;
249:
250: if (!isInSsDir(folderLine)) {
251: // We are only interested in modifications to files in the specified ssdir
252: return null;
253: } else if (isBeforeLastBuild(mod.modifiedTime)) {
254: // We are only interested in modifications since the last build
255: return null;
256: } else if (fileLine.startsWith("Labeled")) {
257: // We don't add labels.
258: return null;
259: } else if (fileLine.startsWith("Checked in")) {
260: String fileName = substringFromLastSlash(folderLine);
261: String folderName = substringToLastSlash(folderLine);
262: Modification.ModifiedFile modfile = mod.createModifiedFile(
263: fileName, folderName);
264:
265: modfile.action = "checkin";
266: mod.comment = parseComment(historyEntry);
267: } else if (fileLine.indexOf(" renamed to ") > -1) {
268: // TODO: This is a special case that is really two modifications: deleted and recovered.
269: // For now I'll consider it a deleted to force a clean build.
270: // I should really make this two modifications.
271: setPropertyOnDelete = deleteModification(historyEntry, mod,
272: fileLine, folderLine);
273: } else if (fileLine.indexOf(" moved to ") > -1) {
274: setPropertyOnDelete = deleteModification(historyEntry, mod,
275: fileLine, folderLine);
276: } else {
277: String fileName = fileLine.substring(0, fileLine
278: .lastIndexOf(" "));
279: Modification.ModifiedFile modfile = mod.createModifiedFile(
280: fileName, folderLine);
281:
282: mod.comment = parseComment(historyEntry);
283:
284: if (fileLine.endsWith("added")) {
285: modfile.action = "add";
286: } else if (fileLine.endsWith("deleted")) {
287: modfile.action = "delete";
288: setPropertyOnDelete = true;
289: } else if (fileLine.endsWith("recovered")) {
290: modfile.action = "recover";
291: } else if (fileLine.endsWith("shared")) {
292: modfile.action = "branch";
293: }
294: }
295:
296: if (setPropertyOnDelete) {
297: properties.deletionFound();
298: }
299:
300: return mod;
301: }
302:
303: private boolean deleteModification(List historyEntry,
304: Modification mod, String fileLine, String folderLine) {
305: mod.comment = parseComment(historyEntry);
306:
307: String fileName = fileLine.substring(0, fileLine.indexOf(" "));
308:
309: Modification.ModifiedFile modfile = mod.createModifiedFile(
310: fileName, folderLine);
311: modfile.action = "delete";
312: return true;
313: }
314:
315: /**
316: * parse comment from vss history (could be multiline)
317: *
318: * @param a
319: * @return the comment
320: */
321: private String parseComment(List a) {
322: StringBuffer comment = new StringBuffer();
323: for (int i = 4; i < a.size(); i++) {
324: comment.append(a.get(i)).append(" ");
325: }
326: return comment.toString().trim();
327: }
328:
329: /**
330: * Parse date/time from VSS file history
331: *
332: * The nameAndDateLine will look like User: Etucker Date: 6/26/01 Time: 11:53a Sometimes also this User: Aaggarwa
333: * Date: 6/29/:1 Time: 3:40p Note the ":" instead of a "0"
334: *
335: * May give additional DateFormats through the vssjournaldateformat tag. E.g.
336: * <code><vssjournaldateformat format="yy-MM-dd hh:mm"/></code>
337: *
338: * @return Date
339: * @param nameAndDateLine
340: */
341: public Date parseDate(String nameAndDateLine) {
342: // Extract date and time into one string with just one space separating the date from the time
343: String dateString = nameAndDateLine.substring(
344: nameAndDateLine.indexOf("Date:") + 5,
345: nameAndDateLine.indexOf("Time:")).trim();
346:
347: String timeString = nameAndDateLine.substring(
348: nameAndDateLine.indexOf("Time:") + 5).trim();
349:
350: if (!overridenDateFormat) {
351: // Fixup for weird format
352: int indexOfColon = dateString.indexOf("/:");
353: if (indexOfColon != -1) {
354: dateString = dateString.substring(0, indexOfColon)
355: + dateString.substring(indexOfColon,
356: indexOfColon + 2).replace(':', '0')
357: + dateString.substring(indexOfColon + 2);
358: }
359:
360: }
361: StringBuffer dateToParse = new StringBuffer();
362: dateToParse.append(dateString);
363: dateToParse.append(" ");
364: dateToParse.append(timeString);
365: if (!overridenDateFormat) {
366: // the am/pm marker of java.text.SimpleDateFormat is 'am' or 'pm'
367: // but we have 'a' or 'p' in default VSS logs with default time format
368: // (for example '6:08p' instead of '6:08pm')
369: dateToParse.append("m");
370: }
371: try {
372: return vssDateTimeFormat.parse(dateToParse.toString());
373:
374: } catch (ParseException pe) {
375: LOG.error("Could not parse date in VssJournal file : "
376: + dateToParse.toString(), pe);
377: }
378: return null;
379: }
380:
381: /**
382: * Parse username from VSS file history
383: *
384: * @param userLine
385: * @return the user name who made the modification
386: */
387: public String parseUser(String userLine) {
388: final int startOfUserName = 6;
389:
390: try {
391: return userLine.substring(startOfUserName,
392: userLine.indexOf("Date: ") - 1).trim();
393: } catch (StringIndexOutOfBoundsException e) {
394: LOG.error("Unparsable string was: " + userLine);
395: throw e;
396: }
397:
398: }
399:
400: /**
401: * Returns the substring of the given string from the last "/" character. UNLESS the last slash character is the
402: * last character or the string does not contain a slash. In that case, return the whole string.
403: */
404: public String substringFromLastSlash(String input) {
405: int lastSlashPos = input.lastIndexOf("/");
406: if (lastSlashPos > 0 && lastSlashPos + 1 <= input.length()) {
407: return input.substring(lastSlashPos + 1);
408: }
409:
410: return input;
411: }
412:
413: /**
414: * Returns the substring of the given string from the beginning to the last "/" character or till the end of the
415: * string if no slash character exists.
416: */
417: public String substringToLastSlash(String input) {
418: int lastSlashPos = input.lastIndexOf("/");
419: if (lastSlashPos > 0) {
420: return input.substring(0, lastSlashPos);
421: }
422:
423: return input;
424: }
425:
426: /**
427: * Determines if the given folder is in the ssdir specified for this VssJournalElement.
428: */
429: protected boolean isInSsDir(String path) {
430: boolean isInDir = (path.toLowerCase().startsWith(ssDir
431: .toLowerCase()));
432: if (isInDir) {
433: // exclude similarly prefixed paths
434: if (ssDir.equalsIgnoreCase(path) // is exact same as ssDir (this happens)
435: || ('/' == path.charAt(ssDir.length())) // subdirs below matching ssDir
436: || "$/".equalsIgnoreCase(ssDir)) { // everything is included
437:
438: // do nothing
439: } else {
440: // is not really in subdir
441: isInDir = false;
442: }
443: }
444: return isInDir;
445: }
446:
447: /**
448: * Determines if the date given is before the last build for this VssJournalElement.
449: */
450: protected boolean isBeforeLastBuild(Date date) {
451: return date.before(lastBuild);
452: }
453: }
|