001: /********************************************************************************
002: * CruiseControl, a Continuous Integration Toolkit
003: * Copyright (c) 2006, 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.labelincrementers;
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.util.ArrayList;
044: import java.util.List;
045: import java.util.StringTokenizer;
046:
047: import net.sourceforge.cruisecontrol.CruiseControlException;
048: import net.sourceforge.cruisecontrol.LabelIncrementer;
049: import net.sourceforge.cruisecontrol.util.Commandline;
050: import net.sourceforge.cruisecontrol.util.ValidationHelper;
051: import net.sourceforge.cruisecontrol.util.IO;
052: import net.sourceforge.cruisecontrol.util.StreamLogger;
053:
054: import org.apache.log4j.Logger;
055: import org.apache.tools.ant.BuildException;
056: import org.apache.tools.ant.Project;
057: import org.apache.tools.ant.taskdefs.Delete;
058: import org.apache.tools.ant.types.FileSet;
059: import org.apache.tools.ant.types.PatternSet.NameEntry;
060: import org.jdom.Element;
061:
062: /**
063: * This class uses the most current changelist of the user in Perforce as the
064: * label for the builds. It can also sync the Perforce managed files to that
065: * changelist number, as well as clean out the existing managed files.
066: *
067: * @author <a href="mailto:groboclown@users.sourceforge.net">Matt Albrecht</a>
068: */
069: public class P4ChangelistLabelIncrementer implements LabelIncrementer {
070:
071: private static final Logger LOG = Logger
072: .getLogger(P4ChangelistLabelIncrementer.class);
073: private static final String CHANGELIST_PREFIX = "@";
074: private static final String REVISION_PREFIX = "#";
075: private static final String RECURSE_U = "/...";
076: private static final String RECURSE_W = "\\...";
077:
078: private String p4Port;
079: private String p4Client;
080: private String p4User;
081: private String p4View;
082: private String p4Passwd;
083:
084: private boolean clean = false;
085: private boolean delete = false;
086: private boolean sync = true;
087:
088: private int baseChangelist = -1;
089:
090: /**
091: * Retrieves the current changelist, or, if given, the specified changelist,
092: * and also performs any necessary actions the user requested.
093: *
094: * @param oldLabel Label from previous successful build.
095: * @return Label to use for most recent successful build.
096: */
097: public String incrementLabel(String oldLabel, Element buildLog) {
098: String label = null;
099: try {
100: validate();
101:
102: // Perform conditional actions.
103: // Since the settings might change or be executed in any order,
104: // we perform the checks on which actions to run here.
105: boolean delTree = delete;
106: boolean cleanP4 = delTree || clean;
107: boolean syncP4 = cleanP4 || sync;
108:
109: if (cleanP4) {
110: LOG.info("Cleaning Perforce clientspec " + p4Client);
111: syncTo(REVISION_PREFIX + 0);
112: }
113: if (delTree) {
114: deleteView();
115: }
116:
117: label = getDefaultLabel();
118:
119: if (syncP4) {
120: syncTo(CHANGELIST_PREFIX + label);
121: }
122: } catch (CruiseControlException cce) {
123: LOG.warn("Couldn't run expected tasks", cce);
124: }
125:
126: return label;
127: }
128:
129: public boolean isPreBuildIncrementer() {
130: // This only has use when used as a pre-build incrementer
131: return true;
132: }
133:
134: /**
135: * Verify that the label specified -- the previous label -- is a valid label.
136: * In this case any label is valid because the next label will not be based on
137: * previous label but on information from Perforce.
138: */
139: public boolean isValidLabel(String label) {
140: return true;
141: }
142:
143: /**
144: * The instance must be fully initialized before calling this method.
145: * @throws IllegalStateException if the instance is not properly initialized
146: */
147: public String getDefaultLabel() {
148: if (baseChangelist > 0) {
149: return Integer.toString(baseChangelist);
150: }
151: // else
152:
153: try {
154: validate();
155:
156: return getCurrentChangelist();
157: } catch (CruiseControlException cce) {
158: cce.printStackTrace();
159: LOG.fatal("Problem accessing Perforce changelist", cce);
160: throw new IllegalStateException(
161: "Problem accessing Perforce changelist");
162: }
163: }
164:
165: // User settings
166:
167: /**
168: * Set the changelist number that you want to build at. If this isn't
169: * set, then the class will get the most current submitted changelist
170: * number. Note that setting this will cause the build to ALWAYS build
171: * at this changelist number.
172: *
173: * @param syncChange the changelist number to perform the sync to.
174: */
175: public void setChangelist(int syncChange) {
176: baseChangelist = syncChange;
177: }
178:
179: public void setPort(String p4Port) {
180: this .p4Port = p4Port;
181: }
182:
183: public void setClient(String p4Client) {
184: this .p4Client = p4Client;
185: }
186:
187: public void setUser(String p4User) {
188: this .p4User = p4User;
189: }
190:
191: public void setView(String p4View) {
192: this .p4View = p4View;
193: }
194:
195: public void setPasswd(String p4Passwd) {
196: this .p4Passwd = p4Passwd;
197: }
198:
199: /**
200: * Disables the label incrementer from synchronizing Perforce to the
201: * view.
202: *
203: * @param b
204: */
205: public void setNoSync(boolean b) {
206: this .sync = !b;
207: }
208:
209: /**
210: * Perform a "p4 sync -f [view]#0" before syncing anew. This will force
211: * the sync to happen.
212: *
213: * @param b
214: */
215: public void setClean(boolean b) {
216: this .clean = b;
217: }
218:
219: /**
220: * Perform a recursive delete of the clientspec view. This
221: * will force a clean & sync. Note that this can potentially
222: * be very destructive, so use with the utmost caution.
223: *
224: * @param b
225: */
226: public void setDelete(boolean b) {
227: this .delete = b;
228: }
229:
230: public void validate() throws CruiseControlException {
231: ValidationHelper.assertIsSet(p4View, "view", this .getClass());
232: ValidationHelper
233: .assertNotEmpty(p4View, "view", this .getClass());
234: ValidationHelper.assertNotEmpty(p4Client, "client", this
235: .getClass());
236: ValidationHelper
237: .assertNotEmpty(p4Port, "port", this .getClass());
238: ValidationHelper
239: .assertNotEmpty(p4User, "user", this .getClass());
240: ValidationHelper.assertNotEmpty(p4Passwd, "passwd", this
241: .getClass());
242: }
243:
244: protected String getCurrentChangelist()
245: throws CruiseControlException {
246: Commandline cmd = buildBaseP4Command();
247: cmd.createArgument("changes");
248: cmd.createArgument("-m1");
249: cmd.createArgument("-ssubmitted");
250:
251: ParseChangelistNumbers pcn = new ParseChangelistNumbers();
252: runP4Cmd(cmd, pcn);
253:
254: String[] changes = pcn.getChangelistNumbers();
255: if (changes != null && changes.length == 1) {
256: return changes[0];
257: } else {
258: throw new CruiseControlException(
259: "Could not discover the changelist");
260: }
261: }
262:
263: protected void syncTo(String viewArg) throws CruiseControlException {
264: Commandline cmd = buildBaseP4Command();
265: cmd.createArguments("sync", p4View + viewArg);
266:
267: runP4Cmd(cmd, new P4CmdParserAdapter());
268: }
269:
270: protected void deleteView() throws CruiseControlException {
271: // despite what people tell you, deleting correctly in Java is
272: // hard. So, let Ant do our dirty work for us.
273: try {
274: Project p = createProject();
275: FileSet fs = getWhereView(p);
276: Delete d = createDelete(p);
277: d.setProject(p);
278: d.setVerbose(true);
279: d.addFileset(fs);
280: d.execute();
281: } catch (BuildException be) {
282: throw new CruiseControlException(be.getMessage(), be);
283: }
284: }
285:
286: /**
287: * If the view mapping contains a reference to a single file,
288: *
289: * @return the collection of recursive directories inside the Perforce
290: * view.
291: * @throws CruiseControlException
292: */
293: protected FileSet getWhereView(Project p)
294: throws CruiseControlException {
295: String view = p4View;
296: if (view == null) {
297: view = "//...";
298: }
299: if (!view.endsWith(RECURSE_U) && !view.endsWith(RECURSE_W)) {
300: // we'll only care about the recursive view. Anything else
301: // should be handled by the sync view#0
302: LOG.debug("view [" + view + "] isn't recursive.");
303: return null;
304: }
305: Commandline cmd = buildBaseP4Command();
306: cmd.createArguments("where", view);
307:
308: ParseOutputParam pop = new ParseOutputParam("");
309: runP4Cmd(cmd, pop);
310: String[] values = pop.getValues();
311: if (values == null || values.length <= 0) {
312: LOG.debug("Didn't find any files for view");
313: return null;
314: }
315: FileSet fs = createFileSet(p);
316:
317: // on windows, this is considered higher than the drive letter.
318: fs.setDir(new File("/"));
319: int count = 0;
320:
321: for (int i = 0; i < values.length; ++i) {
322: // first token: the depot name
323: // second token: the client name
324: // third token+: the local file system name
325:
326: // like above, we only care about the recursive view. If the
327: // line doesn't end in /... or \... (even if it's a %%1), we ignore
328: // it. This makes our life so much simpler when dealing with
329: // spaces.
330: String s = values[i];
331: //LOG.debug("Parsing view line " + i + " [" + s + "]");
332: if (!s.endsWith(RECURSE_U) && !s.endsWith(RECURSE_W)) {
333: continue;
334: }
335:
336: String[] tokens = new String[3];
337: int pos = 0;
338: for (int j = 0; j < 3; ++j) {
339: StringBuffer sb = new StringBuffer();
340: boolean neot = true;
341: while (neot) {
342: if (pos >= s.length()) {
343: break;
344: }
345: int q1 = s.indexOf('\'', pos);
346: int q2 = s.indexOf('"', pos);
347: int sp = s.indexOf(' ', pos);
348: if (q1 >= 0 && (q1 < q2 || q2 < 0)
349: && (q1 < sp || sp < 0)) {
350: sb.append(s.substring(pos, q1));
351: pos = q1 + 1;
352: } else if (q2 >= 0 && (q2 < q1 || q1 < 0)
353: && (q2 < sp || sp < 0)) {
354: sb.append(s.substring(pos, q2));
355: pos = q2 + 1;
356: } else if (sp >= 0) {
357: // check if we're at the end of the token
358: String sub = s.substring(pos, sp);
359: pos = sp + 1;
360: sb.append(sub);
361: if (sub.endsWith(RECURSE_U)
362: || sub.endsWith(RECURSE_W)) {
363: neot = false;
364: } else {
365: // keep the space - it's inside the token
366: sb.append(' ');
367: }
368: } else {
369: sb.append(s.substring(pos));
370: neot = false;
371: }
372: }
373: tokens[j] = new String(sb).trim();
374: }
375: if (tokens[0] != null
376: && tokens[1] != null
377: && tokens[2] != null
378: && (tokens[2].endsWith(RECURSE_U) || tokens[2]
379: .endsWith(RECURSE_W))) {
380: // convert the P4 recurse expression with the Ant
381: // recurse expression
382: String f = tokens[2].substring(0, tokens[2].length()
383: - RECURSE_W.length())
384: + File.separator + "**";
385: // a - in front of the depot name means to exclude this path
386: if (tokens[0].startsWith("-//")) {
387: NameEntry ne = fs.createExclude();
388: ne.setName(f);
389: } else {
390: NameEntry ne = fs.createInclude();
391: ne.setName(f);
392: }
393: ++count;
394: }
395: }
396: if (count > 0) {
397: return fs;
398: } else {
399: LOG.debug("no files in view to delete");
400: return null;
401: }
402: }
403:
404: protected Project createProject() {
405: Project p = new Project();
406: p.init();
407: return p;
408: }
409:
410: protected Delete createDelete(Project p)
411: throws CruiseControlException {
412: Object o = p.createTask("delete");
413: if (o == null || !(o instanceof Delete)) {
414: // Backup code just in case we didn't work right.
415: // If we can guarantee the above operation works all the time,
416: // then this log note should be replaced with an exception.
417: LOG
418: .info("Could not find <delete> task in Ant. Defaulting to basic constructor.");
419: Delete d = new Delete();
420: d.setProject(p);
421: o = d;
422: }
423: return (Delete) o;
424: }
425:
426: protected FileSet createFileSet(Project p)
427: throws CruiseControlException {
428: Object o = p.createDataType("fileset");
429: if (o == null || !(o instanceof FileSet)) {
430: // Backup code just in case we didn't work right.
431: // If we can guarantee the above operation works all the time,
432: // then this log note should be replaced with an exception.
433: LOG
434: .info("Could not find <fileset> type in Ant. Defaulting to basic constructor.");
435: FileSet fs = new FileSet();
436: fs.setProject(p);
437: o = fs;
438: }
439: return (FileSet) o;
440: }
441:
442: protected Commandline buildBaseP4Command() {
443: Commandline commandLine = new Commandline();
444: commandLine.setExecutable("p4");
445: commandLine.createArgument("-s");
446:
447: if (p4Client != null) {
448: commandLine.createArguments("-c", p4Client);
449: }
450:
451: if (p4Port != null) {
452: commandLine.createArguments("-p", p4Port);
453: }
454:
455: if (p4User != null) {
456: commandLine.createArguments("-u", p4User);
457: }
458:
459: if (p4Passwd != null) {
460: commandLine.createArguments("-P", p4Passwd);
461: }
462: return commandLine;
463: }
464:
465: protected void runP4Cmd(Commandline cmd, P4CmdParser parser)
466: throws CruiseControlException {
467: try {
468: Process p = cmd.execute();
469:
470: try {
471: Thread stderr = new Thread(StreamLogger.getWarnPumper(
472: LOG, p));
473: stderr.start();
474:
475: InputStream p4Stream = p.getInputStream();
476: parseStream(p4Stream, parser);
477: stderr.join();
478: } finally {
479: p.waitFor();
480: IO.close(p);
481: }
482: } catch (IOException e) {
483: throw new CruiseControlException(
484: "Problem trying to execute command line process", e);
485: } catch (InterruptedException e) {
486: throw new CruiseControlException(
487: "Problem trying to execute command line process", e);
488: }
489: }
490:
491: protected void parseStream(InputStream stream, P4CmdParser parser)
492: throws IOException {
493: String line;
494: BufferedReader reader = new BufferedReader(
495: new InputStreamReader(stream));
496: while ((line = reader.readLine()) != null) {
497: if (line.startsWith("error:")) {
498: throw new IOException(
499: "Error reading P4 stream: P4 says: " + line);
500: } else if (line.startsWith("exit: 0")) {
501: LOG.debug("p4cmd: Found exit 0");
502: break;
503: } else if (line.startsWith("exit:")) {
504: // not an exit code of 0
505: LOG.error("p4cmd: Found exit " + line);
506: throw new IOException(
507: "Error reading P4 stream: P4 says: " + line);
508: } else if (line.startsWith("warning:")) {
509: parser.warning(line.substring(8));
510: } else if (line.startsWith("info:")
511: || line.startsWith("info1:")) {
512: parser.info(line.substring(5));
513: } else if (line.startsWith("text:")) {
514: parser.text(line.substring(5));
515: }
516: }
517: if (line == null) {
518: throw new IOException(
519: "Error reading P4 stream: Unexpected EOF reached");
520: }
521: }
522:
523: protected static interface P4CmdParser {
524: public void warning(String msg);
525:
526: public void info(String msg);
527:
528: public void text(String msg);
529: }
530:
531: protected static class P4CmdParserAdapter implements P4CmdParser {
532: public void warning(String msg) {
533: // empty
534: }
535:
536: public void info(String msg) {
537: // empty
538: }
539:
540: public void text(String msg) {
541: // empty
542: }
543: }
544:
545: protected static class ParseChangelistNumbers extends
546: P4CmdParserAdapter {
547: private ArrayList changelists = new ArrayList();
548:
549: public void info(String msg) {
550: StringTokenizer st = new StringTokenizer(msg);
551: st.nextToken(); // skip 'Change' text
552: changelists.add(st.nextToken());
553: }
554:
555: public String[] getChangelistNumbers() {
556: String[] changelistNumbers = new String[0];
557: return (String[]) changelists.toArray(changelistNumbers);
558: }
559: }
560:
561: protected static class ParseOutputParam extends P4CmdParserAdapter {
562: public ParseOutputParam(String paramName) {
563: this .paramName = paramName;
564: }
565:
566: private final String paramName;
567: private List values = new ArrayList();
568:
569: public void info(final String msg) {
570: String m = msg.trim();
571: if (m.startsWith(paramName)) {
572: String m2 = m.substring(paramName.length()).trim();
573: values.add(m2);
574: }
575: }
576:
577: public String[] getValues() {
578: String[] v = new String[0];
579: return (String[]) values.toArray(v);
580: }
581: }
582: }
|