001: /********************************************************************************
002: * CruiseControl, a Continuous Integration Toolkit
003: * Copyright (c) 2007, 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 net.sourceforge.cruisecontrol.CruiseControlException;
039: import net.sourceforge.cruisecontrol.Modification;
040: import net.sourceforge.cruisecontrol.SourceControl;
041: import net.sourceforge.cruisecontrol.util.Commandline;
042: import net.sourceforge.cruisecontrol.util.IO;
043: import net.sourceforge.cruisecontrol.util.StreamLogger;
044: import net.sourceforge.cruisecontrol.util.ValidationHelper;
045: import org.apache.log4j.Logger;
046: import org.jdom.Document;
047: import org.jdom.Element;
048: import org.jdom.JDOMException;
049: import org.jdom.input.SAXBuilder;
050:
051: import java.io.File;
052: import java.io.IOException;
053: import java.io.InputStream;
054: import java.io.InputStreamReader;
055: import java.io.Reader;
056: import java.text.DateFormat;
057: import java.text.ParseException;
058: import java.text.SimpleDateFormat;
059: import java.util.ArrayList;
060: import java.util.Date;
061: import java.util.Iterator;
062: import java.util.List;
063: import java.util.Map;
064: import java.util.StringTokenizer;
065: import java.util.TimeZone;
066:
067: /**
068: * This class implements the SourceControl methods for a Store repository, which
069: * is the version control system used by Cincom Smalltalk Visualworks.
070: *
071: * @see <a href="http://smalltalk.cincom.com/">smalltalk.cincom.com</a>
072: * @author <a href="rcoulman@gmail.com">Randy Coulman</a>
073: */
074: public class Store implements SourceControl {
075:
076: private static final Logger LOG = Logger.getLogger(Store.class);
077:
078: /** Date format expected by Store */
079: private static final String STORE_DATE_FORMAT = "MM/dd/yyyy HH:mm:ss.SSS";
080:
081: private final SourceControlProperties properties = new SourceControlProperties();
082:
083: /** Configuration parameters */
084: private String workingDirectory;
085: private String script;
086: private String profile;
087: private List packages;
088: private String versionRegex;
089: private String minimumBlessingLevel;
090: private String parcelBuilderFile;
091:
092: public Map getProperties() {
093: return properties.getPropertiesAndReset();
094: }
095:
096: public void setProperty(String property) {
097: properties.assignPropertyName(property);
098: }
099:
100: /**
101: * Sets the working directory to use when interacting with Store.
102: *
103: * @param directory String indicating the directory to use as the
104: * working directory
105: */
106: public void setWorkingDirectory(String directory) {
107: this .workingDirectory = directory;
108: }
109:
110: /**
111: * Sets the script to use to make calls to Store.
112: *
113: * This script should start a VisualWorks image with the CruiseControl
114: * package loaded and pass on the rest of the command-line arguments
115: * supplied by this plugin.
116: *
117: * @param script String indicating the executable script to
118: * use when making calls to Store.
119: */
120: public void setScript(String script) {
121: this .script = script;
122: }
123:
124: /**
125: * Sets the name of the Store profile to check for modifications.
126: *
127: * @param profile String indicating the name of the Store profile to
128: * connect to when checking for modifications
129: */
130: public void setProfile(String profile) {
131: this .profile = profile;
132: }
133:
134: /**
135: * Sets the list of Store packages to be checked.
136: *
137: * @param packageNames a comma-separated list of package names
138: */
139: public void setPackages(String packageNames) {
140: if (packageNames != null) {
141: StringTokenizer st = new StringTokenizer(packageNames, ",");
142: this .packages = new ArrayList();
143: while (st.hasMoreTokens()) {
144: this .packages.add(st.nextToken());
145: }
146: }
147: }
148:
149: /**
150: * Sets a regex to use to select versions of interest.
151: *
152: * @param regex String containing a regular expression that
153: * matches versions of interest
154: */
155: public void setVersionRegex(String regex) {
156: this .versionRegex = regex;
157: }
158:
159: /**
160: * Sets a minimum blessing level to select versions of interest.
161: *
162: * @param blessing String containing the minimum blessing level
163: * that package versions must have to be included
164: */
165: public void setMinimumBlessingLevel(String blessing) {
166: this .minimumBlessingLevel = blessing;
167: }
168:
169: /**
170: * Sets the name of a file to store the list of head package versions.
171: *
172: * @param filename String containing the filename used to store input
173: * for ParcelBuilder to use to deploy parcels
174: */
175: public void setParcelBuilderFile(String filename) {
176: this .parcelBuilderFile = filename;
177: }
178:
179: /**
180: * This method validates that at least the repository location or the local
181: * working copy location has been specified.
182: *
183: * @throws CruiseControlException Thrown when the repository location and
184: * the local working copy location are both
185: * null
186: */
187: public void validate() throws CruiseControlException {
188: ValidationHelper
189: .assertTrue(workingDirectory != null,
190: "'workingDirectory is a required attribute on the Store task");
191: if (workingDirectory != null) {
192: File directory = new File(workingDirectory);
193: ValidationHelper.assertTrue(directory.exists()
194: && directory.isDirectory(),
195: "'workingDirectory' must be an existing directory. Was "
196: + directory.getAbsolutePath());
197: }
198:
199: ValidationHelper.assertTrue(script != null,
200: "'script' is a required attribute on the Store task");
201: if (script != null) {
202: File scriptFile = new File(script);
203: ValidationHelper.assertTrue(scriptFile.exists(),
204: "'script' must be an existing file. Was "
205: + scriptFile.getAbsolutePath());
206: }
207:
208: ValidationHelper.assertTrue(profile != null,
209: "'profile' is a required attribute on the Store task");
210: ValidationHelper.assertTrue(packages != null,
211: "'packages' is a required attribute on the Store task");
212: ValidationHelper.assertTrue(packages.size() > 0,
213: "'packages' must specify at least one package");
214: }
215:
216: /**
217: * Returns a list of modifications detailing all the changes between
218: * the last build and the latest revision in the repository.
219: * @return the list of modifications, or an empty list if we failed
220: * to retrieve the changes.
221: */
222: public List getModifications(Date lastBuild, Date now) {
223: List modifications = new ArrayList();
224: Commandline command;
225: try {
226: command = buildCommand(lastBuild, now);
227: } catch (CruiseControlException e) {
228: LOG.error("Error building history command", e);
229: return modifications;
230: }
231: try {
232: modifications = execCommand(command);
233: } catch (Exception e) {
234: LOG.error("Error executing svn log command " + command, e);
235: }
236: fillPropertiesIfNeeded(modifications);
237: return modifications;
238: }
239:
240: /**
241: * Generates the command line for the store log command.
242: *
243: * For example:
244: *
245: * 'storeScript -profile local -packages PackageA "Package B" -lastBuild {lastbuildtime} -now {currentTime} -check'
246: */
247: Commandline buildCommand(Date lastBuild, Date checkTime)
248: throws CruiseControlException {
249: Commandline command = new Commandline();
250: command.setWorkingDirectory(workingDirectory);
251: command.setExecutable(script);
252:
253: command.createArguments("-profile", profile);
254: command.createArgument("-packages");
255: for (Iterator iterator = packages.iterator(); iterator
256: .hasNext();) {
257: command.createArgument((String) iterator.next());
258: }
259: if (versionRegex != null) {
260: command.createArguments("-versionRegex", versionRegex);
261: }
262: if (minimumBlessingLevel != null) {
263: command.createArguments("-blessedAtLeast",
264: minimumBlessingLevel);
265: }
266: command.createArguments("-lastBuild", formatDate(lastBuild));
267: command.createArguments("-now", formatDate(checkTime));
268: if (parcelBuilderFile != null) {
269: command.createArguments("-parcelBuilderFile",
270: parcelBuilderFile);
271: }
272: command.createArgument("-check");
273:
274: LOG.debug("Executing command: " + command);
275:
276: return command;
277: }
278:
279: static String formatDate(Date date) {
280: return getDateFormatter().format(date);
281: }
282:
283: private List execCommand(Commandline command)
284: throws InterruptedException, IOException, ParseException,
285: JDOMException {
286:
287: Process p = command.execute();
288:
289: Thread stderr = logErrorStream(p);
290: InputStream storeStream = p.getInputStream();
291: List modifications = parseStream(storeStream);
292:
293: p.waitFor();
294: stderr.join();
295: IO.close(p);
296:
297: return modifications;
298: }
299:
300: private static Thread logErrorStream(Process p) {
301: Thread stderr = new Thread(StreamLogger.getWarnPumper(LOG, p
302: .getErrorStream()));
303: stderr.start();
304: return stderr;
305: }
306:
307: private List parseStream(InputStream storeStream)
308: throws JDOMException, IOException, ParseException {
309:
310: final InputStreamReader reader = new InputStreamReader(
311: storeStream, "UTF-8");
312: try {
313: return StoreLogXMLParser.parse(reader);
314: } finally {
315: reader.close();
316: }
317: }
318:
319: void fillPropertiesIfNeeded(List modifications) {
320: if (!modifications.isEmpty()) {
321: properties.modificationFound();
322: }
323: }
324:
325: public static DateFormat getDateFormatter() {
326: DateFormat f = new SimpleDateFormat(STORE_DATE_FORMAT);
327: f.setTimeZone(TimeZone.getTimeZone("GMT"));
328: return f;
329: }
330:
331: static final class StoreLogXMLParser {
332:
333: private StoreLogXMLParser() {
334: }
335:
336: static List parse(Reader reader) throws ParseException,
337: JDOMException, IOException {
338:
339: SAXBuilder builder = new SAXBuilder(false);
340: Document document = builder.build(reader);
341: return parseDOMTree(document);
342: }
343:
344: static List parseDOMTree(Document document)
345: throws ParseException {
346: List modifications = new ArrayList();
347:
348: Element rootElement = document.getRootElement();
349: List packageEntries = rootElement.getChildren("package");
350: for (Iterator iterator = packageEntries.iterator(); iterator
351: .hasNext();) {
352: Element packageEntry = (Element) iterator.next();
353:
354: List modificationsOfRevision = parsePackageEntry(packageEntry);
355: modifications.addAll(modificationsOfRevision);
356: }
357:
358: return modifications;
359: }
360:
361: static List parsePackageEntry(Element packageEntry)
362: throws ParseException {
363: List modifications = new ArrayList();
364:
365: List blessings = packageEntry.getChildren("blessing");
366: for (Iterator iterator = blessings.iterator(); iterator
367: .hasNext();) {
368: Element blessing = (Element) iterator.next();
369:
370: Modification modification = new Modification("store");
371:
372: modification.modifiedTime = convertDate(blessing
373: .getAttributeValue("timestamp"));
374: modification.userName = blessing
375: .getAttributeValue("user");
376: modification.comment = blessing.getText();
377: modification.revision = packageEntry
378: .getAttributeValue("version");
379:
380: Modification.ModifiedFile modfile = modification
381: .createModifiedFile(packageEntry
382: .getAttributeValue("name"), null);
383: modfile.action = packageEntry
384: .getAttributeValue("action");
385: modfile.revision = modification.revision;
386:
387: modifications.add(modification);
388: }
389:
390: return modifications;
391: }
392:
393: /**
394: * Converts the specified Store date string into a Date.
395: * @param date with format "MM/dd/yyyy HH:mm:ss.SSS"
396: * @return converted date
397: * @throws ParseException if specified date doesn't match the expected format
398: */
399: static Date convertDate(String date) throws ParseException {
400: return getDateFormatter().parse(date);
401: }
402: }
403: }
|