/********************************************************************************
* 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.BufferedReader;
import java.io.FileReader;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import net.sourceforge.cruisecontrol.CruiseControlException;
import net.sourceforge.cruisecontrol.Modification;
import net.sourceforge.cruisecontrol.SourceControl;
import net.sourceforge.cruisecontrol.util.ValidationHelper;
import net.sourceforge.cruisecontrol.util.IO;
import org.apache.log4j.Logger;
/**
* This class handles all VSS-related aspects of determining the modifications since the last good build.
*
* This class uses Source Safe Journal files. Unlike the history files that are generated by executing
* <code>ss.exe history</code>, journal files must be setup by the Source Safe administrator before the point that
* logging of modifications is to occur.
*
* This code has been tested against Visual Source Safe v6.0 build 8383.
*
* @author Eli Tucker
* @author <a href="mailto:alden@thoughtworks.com">alden almagro</a>
* @author <a href="mailto:jcyip@thoughtworks.com">Jason Yip</a>
* @author Arun Aggarwal
* @author Jonny Boman
* @author <a href="mailto:simon.brandhof@hortis.ch">Simon Brandhof</a>
*/
public class VssJournal implements SourceControl {
private static final Logger LOG = Logger.getLogger(VssJournal.class);
private String dateFormat;
private String timeFormat;
private SimpleDateFormat vssDateTimeFormat;
private boolean overridenDateFormat = false;
private String ssDir = "$/";
private String journalFile;
private SourceControlProperties properties = new SourceControlProperties();
private Date lastBuild;
private final ArrayList<Modification> modifications = new ArrayList<Modification>();
public VssJournal() {
dateFormat = "MM/dd/yy";
timeFormat = "hh:mma";
constructVssDateTimeFormat();
}
/**
* @param s the project to get history from
*/
public void setSsDir(String s) {
StringBuffer sb = new StringBuffer();
if (!s.startsWith("$")) {
sb.append("$");
}
if (s.endsWith("/")) {
sb.append(s.substring(0, s.length() - 1));
} else {
sb.append(s);
}
this.ssDir = sb.toString();
}
/**
* @param journalFile Full path to journal file. Example: <code>c:/vssdata/journal/journal.txt</code>
*/
public void setJournalFile(String journalFile) {
this.journalFile = journalFile;
}
/**
* @param property a property to be set if the project has modifications if we have a change that only
* requires repackaging, i.e. jsp, we don't need to recompile everything, just rejar.
*/
public void setProperty(String property) {
properties.assignPropertyName(property);
}
/**
* Set the name of the property to be set if some files were deleted or renamed from VSS on this project.
*
* @param propertyOnDelete the name of the property to set
*/
public void setPropertyOnDelete(String propertyOnDelete) {
properties.assignPropertyOnDeleteName(propertyOnDelete);
}
/**
* @param format the date format to use for parsing VSS journal.
*
* The default date format is <code>MM/dd/yy</code>. If your VSS server is set to a different region, you may wish
* to use a format such as <code>dd/MM/yy</code>.
*
* @see java.text.SimpleDateFormat
*/
public void setDateFormat(String format) {
dateFormat = format;
overridenDateFormat = true;
constructVssDateTimeFormat();
}
/**
* @param format the time format to use for parsing VSS journal.
*
* The default time format is <code>hh:mma</code> . If your VSS server is set to a different region, you may wish to
* use a format such as <code>HH:mm</code> .
*
* @see java.text.SimpleDateFormat
*/
public void setTimeFormat(String format) {
timeFormat = format;
constructVssDateTimeFormat();
}
private void constructVssDateTimeFormat() {
vssDateTimeFormat = new SimpleDateFormat(dateFormat + " " + timeFormat, Locale.US);
}
/**
* @param lastBuild the _lastBuild date. Protected so it can be used by tests.
*/
protected void setLastBuildDate(Date lastBuild) {
this.lastBuild = lastBuild;
}
public Map<String, String> getProperties() {
return properties.getPropertiesAndReset();
}
public void validate() throws CruiseControlException {
ValidationHelper.assertIsSet(journalFile, "journalfile", this.getClass());
ValidationHelper.assertIsSet(ssDir, "ssdir", this.getClass());
}
/**
* Do the work... I'm writing to a file since VSS will start wrapping lines if I read directly from the stream.
*/
public List<Modification> getModifications(final Date lastBuild, final Date now) {
this.lastBuild = lastBuild;
modifications.clear();
try {
final BufferedReader br = new BufferedReader(new FileReader(journalFile));
try {
String s = br.readLine();
while (s != null) {
final ArrayList<String> entry = new ArrayList<String>();
entry.add(s);
s = br.readLine();
while (s != null && !s.equals("")) {
entry.add(s);
s = br.readLine();
}
Modification mod = handleEntry(entry);
if (mod != null) {
modifications.add(mod);
}
if ("".equals(s)) {
s = br.readLine();
}
}
} finally {
IO.close(br);
}
} catch (Exception e) {
LOG.warn(e);
}
if (modifications.size() > 0) {
properties.modificationFound();
}
LOG.info("Found " + modifications.size() + " modified files");
return modifications;
}
/**
* Parse individual VSS history entry
*
* @param historyEntry individual VSS history entry
* @return a modification
*/
protected Modification handleEntry(List historyEntry) {
Modification mod = new Modification("vss");
String nameAndDateLine = (String) historyEntry.get(2);
mod.userName = parseUser(nameAndDateLine);
mod.modifiedTime = parseDate(nameAndDateLine);
String folderLine = (String) historyEntry.get(0);
String fileLine = (String) historyEntry.get(3);
boolean setPropertyOnDelete = false;
if (!isInSsDir(folderLine)) {
// We are only interested in modifications to files in the specified ssdir
return null;
} else if (isBeforeLastBuild(mod.modifiedTime)) {
// We are only interested in modifications since the last build
return null;
} else if (fileLine.startsWith("Labeled")) {
// We don't add labels.
return null;
} else if (fileLine.startsWith("Checked in")) {
String fileName = substringFromLastSlash(folderLine);
String folderName = substringToLastSlash(folderLine);
Modification.ModifiedFile modfile = mod.createModifiedFile(fileName, folderName);
modfile.action = "checkin";
mod.comment = parseComment(historyEntry);
} else if (fileLine.indexOf(" renamed to ") > -1) {
// TODO: This is a special case that is really two modifications: deleted and recovered.
// For now I'll consider it a deleted to force a clean build.
// I should really make this two modifications.
setPropertyOnDelete = deleteModification(historyEntry, mod, fileLine, folderLine);
} else if (fileLine.indexOf(" moved to ") > -1) {
setPropertyOnDelete = deleteModification(historyEntry, mod, fileLine, folderLine);
} else {
String fileName = fileLine.substring(0, fileLine.lastIndexOf(" "));
Modification.ModifiedFile modfile = mod.createModifiedFile(fileName, folderLine);
mod.comment = parseComment(historyEntry);
if (fileLine.endsWith("added")) {
modfile.action = "add";
} else if (fileLine.endsWith("deleted")) {
modfile.action = "delete";
setPropertyOnDelete = true;
} else if (fileLine.endsWith("recovered")) {
modfile.action = "recover";
} else if (fileLine.endsWith("shared")) {
modfile.action = "branch";
}
}
if (setPropertyOnDelete) {
properties.deletionFound();
}
return mod;
}
private boolean deleteModification(List historyEntry, Modification mod, String fileLine, String folderLine) {
mod.comment = parseComment(historyEntry);
String fileName = fileLine.substring(0, fileLine.indexOf(" "));
Modification.ModifiedFile modfile = mod.createModifiedFile(fileName, folderLine);
modfile.action = "delete";
return true;
}
/**
* @param a comment from vss history (could be multiline)
* @return the comment
*/
private String parseComment(final List a) {
final StringBuilder comment = new StringBuilder();
for (int i = 4; i < a.size(); i++) {
comment.append(a.get(i)).append(" ");
}
return comment.toString().trim();
}
/**
* Parse date/time from VSS file history
*
* The nameAndDateLine will look like User: Etucker Date: 6/26/01 Time: 11:53a Sometimes also this User: Aaggarwa
* Date: 6/29/:1 Time: 3:40p Note the ":" instead of a "0"
*
* May give additional DateFormats through the vssjournaldateformat tag. E.g.
* {@code <vssjournaldateformat format="yy-MM-dd hh:mm"/>}
*
* @return Date
* @param nameAndDateLine will look like User: Etucker Date: 6/26/01 Time: 11:53a Sometimes also this User: Aaggarwa
* Date: 6/29/:1 Time: 3:40p Note the ":" instead of a "0"
*/
public Date parseDate(final String nameAndDateLine) {
// Extract date and time into one string with just one space separating the date from the time
String dateString = nameAndDateLine.substring(
nameAndDateLine.indexOf("Date:") + 5,
nameAndDateLine.indexOf("Time:")).trim();
final String timeString = nameAndDateLine.substring(
nameAndDateLine.indexOf("Time:") + 5).trim();
if (!overridenDateFormat) {
// Fixup for weird format
final int indexOfColon = dateString.indexOf("/:");
if (indexOfColon != -1) {
dateString = dateString.substring(0, indexOfColon)
+ dateString.substring(indexOfColon, indexOfColon + 2).replace(':', '0')
+ dateString.substring(indexOfColon + 2);
}
}
final StringBuilder dateToParse = new StringBuilder();
dateToParse.append(dateString);
dateToParse.append(" ");
dateToParse.append(timeString);
if (!overridenDateFormat) {
// the am/pm marker of java.text.SimpleDateFormat is 'am' or 'pm'
// but we have 'a' or 'p' in default VSS logs with default time format
// (for example '6:08p' instead of '6:08pm')
dateToParse.append("m");
}
try {
return vssDateTimeFormat.parse(dateToParse.toString());
} catch (ParseException pe) {
LOG.error("Could not parse date in VssJournal file : " + dateToParse.toString(), pe);
}
return null;
}
/**
* Parse username from VSS file history
*
* @param userLine username from VSS file history
* @return the user name who made the modification
*/
public String parseUser(String userLine) {
final int startOfUserName = 6;
try {
return userLine.substring(startOfUserName, userLine.indexOf("Date: ") - 1).trim();
} catch (StringIndexOutOfBoundsException e) {
LOG.error("Unparsable string was: " + userLine);
throw e;
}
}
/**
* Returns the substring of the given string from the last "/" character. UNLESS the last slash character is the
* last character or the string does not contain a slash. In that case, return the whole string.
* @param input string to parse
* @return result string
*/
public String substringFromLastSlash(String input) {
int lastSlashPos = input.lastIndexOf("/");
if (lastSlashPos > 0 && lastSlashPos + 1 <= input.length()) {
return input.substring(lastSlashPos + 1);
}
return input;
}
/**
* Returns the substring of the given string from the beginning to the last "/" character or till the end of the
* string if no slash character exists.
* @param input string to parse
* @return result string
*/
public String substringToLastSlash(String input) {
int lastSlashPos = input.lastIndexOf("/");
if (lastSlashPos > 0) {
return input.substring(0, lastSlashPos);
}
return input;
}
/**
* Determines if the given folder is in the ssdir specified for this VssJournalElement.
* @param path a folder path
* @return true if the given folder is in the ssdir specified for this VssJournalElement.
*/
protected boolean isInSsDir(String path) {
boolean isInDir = (path.toLowerCase().startsWith(ssDir.toLowerCase()));
if (isInDir) {
// exclude similarly prefixed paths
if (ssDir.equalsIgnoreCase(path) // is exact same as ssDir (this happens)
|| ('/' == path.charAt(ssDir.length())) // subdirs below matching ssDir
|| "$/".equalsIgnoreCase(ssDir)) { // everything is included
// do nothing
} else {
// is not really in subdir
isInDir = false;
}
}
return isInDir;
}
/**
* @param date the date to compare to the lastBuild date.
* @return true if the date given is before the last build for this VssJournalElement.
*/
protected boolean isBeforeLastBuild(Date date) {
return date.before(lastBuild);
}
}