001: /********************************************************************************
002: * CruiseControl, a Continuous Integration Toolkit
003: * Copyright (c) 2001-2003, 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.IOException;
040: import java.io.InputStream;
041: import java.io.InputStreamReader;
042: import java.text.DateFormat;
043: import java.text.ParseException;
044: import java.text.SimpleDateFormat;
045: import java.util.ArrayList;
046: import java.util.Calendar;
047: import java.util.Date;
048: import java.util.HashMap;
049: import java.util.Iterator;
050: import java.util.List;
051: import java.util.Map;
052: import java.util.NoSuchElementException;
053: import java.util.StringTokenizer;
054:
055: import net.sourceforge.cruisecontrol.CruiseControlException;
056: import net.sourceforge.cruisecontrol.Modification;
057: import net.sourceforge.cruisecontrol.SourceControl;
058: import net.sourceforge.cruisecontrol.util.CommandExecutor;
059: import net.sourceforge.cruisecontrol.util.Commandline;
060: import net.sourceforge.cruisecontrol.util.DiscardConsumer;
061: import net.sourceforge.cruisecontrol.util.IO;
062: import net.sourceforge.cruisecontrol.util.StreamConsumer;
063: import net.sourceforge.cruisecontrol.util.StreamLogger;
064: import net.sourceforge.cruisecontrol.util.StreamPumper;
065: import net.sourceforge.cruisecontrol.util.Util;
066: import net.sourceforge.cruisecontrol.util.ValidationHelper;
067:
068: import org.apache.log4j.Logger;
069: import org.jdom.Element;
070:
071: /**
072: * This class implements the SourceControlElement methods for a P4 depot. The call to CVS is assumed to work without any
073: * setup. This implies that if the authentication type is pserver the call to cvs login should be done prior to calling
074: * this class. <p/> P4Element depends on the optional P4 package delivered with Ant v1.3. But since it probably doesn't
075: * make much sense using the P4Element without other P4 support it shouldn't be a problem. <p/> P4Element sets the
076: * property ${p4element.change} with the latest changelist number or the changelist with the latest date. This should
077: * then be passed into p4sync or other p4 commands.
078: *
079: * @author <a href="mailto:niclas.olofsson@ismobile.com">Niclas Olofsson - isMobile.com</a>
080: * @author <a href="mailto:jcyip@thoughtworks.com">Jason Yip</a>
081: * @author Tim McCune
082: * @author J D Glanville
083: * @author Patrick Conant Copyright (c) 2005 Hewlett-Packard Development Company, L.P.
084: * @author John Lussmyer
085: */
086: public class P4 implements SourceControl {
087:
088: private static final Logger LOG = Logger.getLogger(P4.class);
089:
090: private String p4Port;
091: private String p4Client;
092: private String p4User;
093: private String p4View;
094: private String p4Passwd;
095: private boolean correctForServerTime = true;
096: private boolean useP4Email = true;
097:
098: private final SimpleDateFormat p4RevisionDateFormatter = new SimpleDateFormat(
099: "yyyy/MM/dd:HH:mm:ss");
100: private final SourceControlProperties properties = new SourceControlProperties();
101:
102: private static final String SERVER_DATE = "Server date: ";
103: private static final String P4_SERVER_DATE_FORMAT = "yyyy/MM/dd HH:mm:ss";
104:
105: public void setPort(String p4Port) {
106: this .p4Port = p4Port;
107: }
108:
109: public void setClient(String p4Client) {
110: this .p4Client = p4Client;
111: }
112:
113: public void setUser(String p4User) {
114: this .p4User = p4User;
115: }
116:
117: public void setView(String p4View) {
118: this .p4View = p4View;
119: }
120:
121: public void setPasswd(String p4Passwd) {
122: this .p4Passwd = p4Passwd;
123: }
124:
125: /**
126: * Indicates whether to correct for time differences between the p4 server and the CruiseControl server. Setting the
127: * flag to "true" will correct for both time zone differences and for non-synchronized system clocks.
128: */
129: public void setCorrectForServerTime(boolean flag) {
130: correctForServerTime = flag;
131: }
132:
133: /**
134: * Sets if the Email address for the user should be retrieved from Perforce.
135: *
136: * @param flag
137: * true to retrieve email addresses from perforce.
138: */
139: public void setUseP4Email(boolean flag) {
140: useP4Email = flag;
141: }
142:
143: public void setProperty(String propertyName) {
144: properties.assignPropertyName(propertyName);
145: }
146:
147: public Map getProperties() {
148: return properties.getPropertiesAndReset();
149: }
150:
151: public void validate() throws CruiseControlException {
152: ValidationHelper.assertIsSet(p4Port, "port", this .getClass());
153: ValidationHelper.assertIsSet(p4Client, "client", this
154: .getClass());
155: ValidationHelper.assertIsSet(p4User, "user", this .getClass());
156: ValidationHelper.assertIsSet(p4View, "view", this .getClass());
157: ValidationHelper.assertNotEmpty(p4Passwd, "passwd", this
158: .getClass());
159: }
160:
161: /**
162: * Get a List of modifications detailing all the changes between now and the last build. Return this as an element.
163: * It is not necessary for sourcecontrols to actually do anything other than returning a chunk of XML data back.
164: *
165: * @param lastBuild
166: * time of last build
167: * @param now
168: * time this build started
169: * @return a list of XML elements that contains data about the modifications that took place. If no changes, this
170: * method returns an empty list.
171: */
172: public List getModifications(Date lastBuild, Date now) {
173: List mods = new ArrayList();
174: try {
175: String[] changelistNumbers = collectChangelistSinceLastBuild(
176: lastBuild, now);
177: if (changelistNumbers.length == 0) {
178: return mods;
179: }
180: mods = describeAllChangelistsAndBuildOutput(changelistNumbers);
181: } catch (Exception e) {
182: LOG.error("Log command failed to execute succesfully", e);
183: }
184:
185: if (!mods.isEmpty()) {
186: properties.modificationFound();
187: }
188:
189: return mods;
190: }
191:
192: private List describeAllChangelistsAndBuildOutput(
193: String[] changelistNumbers) throws Exception {
194: Commandline command = buildDescribeCommand(changelistNumbers);
195: LOG.debug(command.toString());
196: Process p = command.execute();
197:
198: Thread error = logErrorStream(p.getErrorStream());
199: InputStream p4Stream = p.getInputStream();
200: List mods = parseChangeDescriptions(p4Stream);
201: getRidOfLeftoverData(p4Stream);
202:
203: // Get the Email address of the user for each changelist
204: if ((mods.size() > 0) && useP4Email) {
205: getEmailAddresses(mods);
206: }
207:
208: p.waitFor();
209: error.join();
210: IO.close(p);
211:
212: return mods;
213: }
214:
215: /**
216: * Get the Email Address of the users who submitted the change lists.
217: *
218: * @param mods
219: * List of P4Modification structures
220: * @throws IOException
221: * @throws InterruptedException
222: */
223: private void getEmailAddresses(List mods) throws IOException,
224: InterruptedException {
225: Iterator iter = mods.iterator();
226: Map users = new HashMap();
227:
228: while (iter.hasNext()) {
229: P4Modification change = (P4Modification) iter.next();
230:
231: if ((change.userName != null)
232: && (change.userName.length() > 0)) {
233: change.emailAddress = (String) users
234: .get(change.userName);
235:
236: if (change.emailAddress == null) {
237: change.emailAddress = getUserEmailAddress(change.userName);
238: users.put(change.userName, change.emailAddress);
239: }
240: }
241:
242: }
243:
244: }
245:
246: /**
247: * Get the Email Address for the given P4 User
248: *
249: * @param username
250: * Perforce user name
251: * @return User Email address if available
252: * @throws IOException
253: * @throws InterruptedException
254: */
255: private String getUserEmailAddress(String username)
256: throws IOException, InterruptedException {
257: String emailaddr = null;
258:
259: Commandline command = buildUserCommand(username);
260: LOG.debug(command.toString());
261: Process p = command.execute();
262:
263: logErrorStream(p.getErrorStream());
264: InputStream p4Stream = p.getInputStream();
265: BufferedReader reader = new BufferedReader(
266: new InputStreamReader(p4Stream));
267:
268: // Find first Changelist item if there is one.
269: String line;
270: while ((line = readToNotPast(reader, "info: Email:",
271: "I really don't care")) != null) {
272: StringTokenizer st = new StringTokenizer(line);
273:
274: try {
275: st.nextToken(); // skip 'info:' text
276: st.nextToken(); // skip 'Email:' text
277: emailaddr = st.nextToken();
278: } catch (NoSuchElementException ex) {
279: // No email address given
280: }
281: }
282:
283: getRidOfLeftoverData(p4Stream);
284:
285: p.waitFor();
286: IO.close(p);
287:
288: return (emailaddr);
289: }
290:
291: private String[] collectChangelistSinceLastBuild(Date lastBuild,
292: Date now) throws Exception {
293: Commandline command = buildChangesCommand(lastBuild, now, Util
294: .isWindows());
295: LOG.debug(command.toString());
296: Process p = command.execute();
297:
298: Thread error = logErrorStream(p.getErrorStream());
299: InputStream p4Stream = p.getInputStream();
300:
301: String[] changelistNumbers = parseChangelistNumbers(p4Stream);
302:
303: p.waitFor();
304: error.join();
305: IO.close(p);
306:
307: return changelistNumbers;
308: }
309:
310: private void getRidOfLeftoverData(InputStream stream) {
311: new StreamPumper(stream, new DiscardConsumer()).run();
312: }
313:
314: protected String[] parseChangelistNumbers(InputStream is)
315: throws IOException {
316: ArrayList changelists = new ArrayList();
317:
318: BufferedReader reader = new BufferedReader(
319: new InputStreamReader(is));
320: String line;
321: while ((line = reader.readLine()) != null) {
322: if (line.startsWith("error:")) {
323: throw new IOException(
324: "Error reading P4 stream: P4 says: " + line);
325: } else if (line.startsWith("exit: 1")) {
326: throw new IOException(
327: "Error reading P4 stream: P4 says: " + line);
328: } else if (line.startsWith("exit: 0")) {
329: break;
330: } else if (line.startsWith("info:")) {
331: StringTokenizer st = new StringTokenizer(line);
332: st.nextToken(); // skip 'info:' text
333: st.nextToken(); // skip 'Change' text
334: changelists.add(st.nextToken());
335: }
336: }
337: if (line == null) {
338: throw new IOException(
339: "Error reading P4 stream: Unexpected EOF reached");
340: }
341: String[] changelistNumbers = new String[0];
342: return (String[]) changelists.toArray(changelistNumbers);
343: }
344:
345: protected List parseChangeDescriptions(InputStream is)
346: throws Exception {
347: int serverOffset = 0;
348: if (correctForServerTime) {
349: serverOffset = (int) calculateServerTimeOffset();
350: }
351:
352: ArrayList changelists = new ArrayList();
353:
354: BufferedReader reader = new BufferedReader(
355: new InputStreamReader(is));
356:
357: // Find first Changelist item if there is one.
358: String line;
359: while ((line = readToNotPast(reader, "text: Change", "exit:")) != null) {
360:
361: P4Modification changelist = new P4Modification();
362: if (line.startsWith("error:")) {
363: throw new IOException(
364: "Error reading P4 stream: P4 says: " + line);
365: } else if (line.startsWith("exit: 1")) {
366: throw new IOException(
367: "Error reading P4 stream: P4 says: " + line);
368: } else if (line.startsWith("exit: 0")) {
369: return changelists;
370: } else if (line.startsWith("text: Change")) {
371: StringTokenizer st = new StringTokenizer(line);
372:
373: st.nextToken(); // skip 'text:' text
374: st.nextToken(); // skip 'Change' text
375: changelist.revision = st.nextToken();
376: st.nextToken(); // skip 'by' text
377:
378: // split user@client
379: StringTokenizer st2 = new StringTokenizer(st
380: .nextToken(), "@");
381: changelist.userName = st2.nextToken();
382: changelist.client = st2.nextToken();
383:
384: st.nextToken(); // skip 'on' text
385: String date = st.nextToken() + ":" + st.nextToken();
386: try {
387: Calendar cal = Calendar.getInstance();
388: cal.setTime(p4RevisionDateFormatter.parse(date));
389: cal.add(Calendar.MILLISECOND, -serverOffset);
390: changelist.modifiedTime = cal.getTime();
391: } catch (ParseException xcp) {
392: changelist.modifiedTime = new Date();
393: }
394: }
395:
396: reader.readLine(); // get past a 'text:'
397: StringBuffer descriptionBuffer = new StringBuffer();
398:
399: // Use this since we don't want the final (empty) line
400: String previousLine = null;
401: line = reader.readLine();
402: while (line != null && line.startsWith("text:")
403: && !line.startsWith("text: Affected files ...")) {
404: if (previousLine != null) {
405: if (descriptionBuffer.length() > 0) {
406: descriptionBuffer.append('\n');
407: }
408: descriptionBuffer.append(previousLine);
409: }
410: try {
411: previousLine = line.substring(5).trim();
412: } catch (Exception e) {
413: LOG
414: .error("Error parsing Perforce description, line that caused problem was: ["
415: + line + "]");
416: }
417:
418: line = reader.readLine();
419: }
420:
421: changelist.comment = descriptionBuffer.toString();
422:
423: // Ok, read affected files if there are any.
424: if (line != null) {
425: reader.readLine(); // read past next 'text:'
426:
427: line = readToNotPast(reader, "info1:", "text:");
428: while (line != null && line.startsWith("info1:")) {
429: String fileName = line.substring(7, line
430: .lastIndexOf("#"));
431: Modification.ModifiedFile affectedFile = changelist
432: .createModifiedFile(fileName, null);
433: affectedFile.action = line.substring(line
434: .lastIndexOf(" ") + 1);
435: affectedFile.revision = line.substring(line
436: .lastIndexOf("#") + 1, line
437: .lastIndexOf(" "));
438:
439: line = readToNotPast(reader, "info1:", "text:");
440: }
441: }
442: changelists.add(changelist);
443: }
444:
445: return changelists;
446: }
447:
448: private Thread logErrorStream(InputStream is) {
449: Thread errorThread = new Thread(StreamLogger.getWarnPumper(LOG,
450: is));
451: errorThread.start();
452: return errorThread;
453: }
454:
455: /**
456: * p4 -s [-c client] [-p port] [-u user] changes -s submitted [view@lastBuildTime@now]
457: *
458: * @throws CruiseControlException
459: */
460: public Commandline buildChangesCommand(Date lastBuildTime,
461: Date now, boolean isWindows) throws CruiseControlException {
462:
463: // If the Perforce server time is different from the CruiseControl
464: // server time, correct the parameter dates for the difference.
465: if (correctForServerTime) {
466: int offset = (int) calculateServerTimeOffset();
467: Calendar cal = Calendar.getInstance();
468:
469: cal.setTime(lastBuildTime);
470: cal.add(Calendar.MILLISECOND, offset);
471: lastBuildTime = cal.getTime();
472:
473: cal.setTime(now);
474: cal.add(Calendar.MILLISECOND, offset);
475: now = cal.getTime();
476: } else {
477: LOG.debug("No server time offset determined.");
478: }
479:
480: Commandline commandLine = buildBaseP4Command();
481:
482: commandLine.createArgument("changes");
483: commandLine.createArguments("-s", "submitted");
484: commandLine.createArgument(p4View + "@"
485: + p4RevisionDateFormatter.format(lastBuildTime) + ",@"
486: + p4RevisionDateFormatter.format(now));
487:
488: return commandLine;
489: }
490:
491: /**
492: * p4 -s [-c client] [-p port] [-u user] describe -s [change number]
493: */
494: public Commandline buildDescribeCommand(String[] changelistNumbers) {
495: Commandline commandLine = buildBaseP4Command();
496:
497: // execP4Command("describe -s " + changeNumber.toString(),
498:
499: commandLine.createArgument("describe");
500: commandLine.createArgument("-s");
501:
502: for (int i = 0; i < changelistNumbers.length; i++) {
503: commandLine.createArgument(changelistNumbers[i]);
504: }
505:
506: return commandLine;
507: }
508:
509: /**
510: * p4 -s [-c client] [-p port] [-u user] user -o [username]
511: */
512: public Commandline buildUserCommand(String username) {
513: Commandline commandLine = buildBaseP4Command();
514: commandLine.createArgument("user");
515: commandLine.createArguments("-o", username);
516:
517: return commandLine;
518: }
519:
520: /**
521: * Calculate the difference in time between the Perforce server and the CruiseControl server. A negative time
522: * difference indicates that the Perforce server time is later than CruiseControl server (e.g. Perforce in New York,
523: * CruiseControl in San Francisco). A positive offset indicates that the Perforce server time is before the
524: * CruiseControl server.
525: *
526: * @throws CruiseControlException
527: */
528: protected long calculateServerTimeOffset()
529: throws CruiseControlException {
530: ServerInfoConsumer serverInfo = new ServerInfoConsumer();
531: CommandExecutor executor = new CommandExecutor(
532: buildInfoCommand());
533: executor.logErrorStreamTo(LOG);
534: executor.setOutputConsumer(serverInfo);
535: executor.executeAndWait();
536: return serverInfo.getOffset();
537: }
538:
539: Commandline buildInfoCommand() {
540: Commandline command = buildBaseP4Command(false);
541: command.createArgument("info");
542: return command;
543: }
544:
545: private Commandline buildBaseP4Command() {
546: boolean prependField = true;
547: return buildBaseP4Command(prependField);
548: }
549:
550: private Commandline buildBaseP4Command(boolean prependField) {
551: Commandline commandLine = new Commandline();
552: commandLine.setExecutable("p4");
553: if (prependField) {
554: commandLine.createArgument("-s");
555: }
556:
557: if (p4Client != null) {
558: commandLine.createArguments("-c", p4Client);
559: }
560:
561: if (p4Port != null) {
562: commandLine.createArguments("-p", p4Port);
563: }
564:
565: if (p4User != null) {
566: commandLine.createArguments("-u", p4User);
567: }
568:
569: if (p4Passwd != null) {
570: commandLine.createArguments("-P", p4Passwd);
571: }
572: return commandLine;
573: }
574:
575: /**
576: * This is a modified version of the one in the CVS element. I found it far more useful if you actually return
577: * either or, because otherwise it would be darn hard to use in places where I actually need the notPast line. Or
578: * did I misunderstand something?
579: */
580: private String readToNotPast(BufferedReader reader,
581: String beginsWith, String notPast) throws IOException {
582:
583: String nextLine = reader.readLine();
584:
585: // (!A && !B) || (!A && !C) || (!B && !C)
586: // !A || !B || !C
587: while (!(nextLine == null || nextLine.startsWith(beginsWith) || nextLine
588: .startsWith(notPast))) {
589: nextLine = reader.readLine();
590: }
591: return nextLine;
592: }
593:
594: private static class P4Modification extends Modification {
595: public String client;
596:
597: public int compareTo(Object o) {
598: P4Modification modification = (P4Modification) o;
599: return getChangelistNumber()
600: - modification.getChangelistNumber();
601: }
602:
603: public boolean equals(Object o) {
604: if (o == null || !(o instanceof P4Modification)) {
605: return false;
606: }
607:
608: P4Modification modification = (P4Modification) o;
609: return getChangelistNumber() == modification
610: .getChangelistNumber();
611: }
612:
613: public int hashCode() {
614: return getChangelistNumber();
615: }
616:
617: private int getChangelistNumber() {
618: return Integer.parseInt(revision);
619: }
620:
621: P4Modification() {
622: super ("p4");
623: }
624:
625: public Element toElement(DateFormat format) {
626:
627: Element element = super .toElement(format);
628: LOG.debug("client = " + client);
629:
630: Element clientElement = new Element("client");
631: clientElement.addContent(client);
632: element.addContent(clientElement);
633:
634: return element;
635: }
636: }
637:
638: static String getQuoteChar(boolean isWindows) {
639: return isWindows ? "\"" : "'";
640: }
641:
642: protected static class ServerInfoConsumer implements StreamConsumer {
643: private boolean found;
644: private long offset;
645: private final SimpleDateFormat p4ServerDateFormatter = new SimpleDateFormat(
646: P4_SERVER_DATE_FORMAT);
647:
648: private Date ccServerTime = new Date();
649:
650: public void consumeLine(String line) {
651: Date p4ServerTime;
652:
653: // Consume the full stream after we have found the offset
654: if (found) {
655: return;
656: }
657:
658: if (line.startsWith(SERVER_DATE)) {
659: try {
660: String dateString = line.substring(SERVER_DATE
661: .length(), SERVER_DATE.length()
662: + P4_SERVER_DATE_FORMAT.length());
663: p4ServerTime = p4ServerDateFormatter
664: .parse(dateString);
665: offset = p4ServerTime.getTime()
666: - ccServerTime.getTime();
667: found = true;
668: } catch (ParseException pe) {
669: LOG
670: .error("Unable to parse p4 server time from line \'"
671: + line
672: + "\'. "
673: + pe.getMessage()
674: + "; Proceeding without time offset.");
675: }
676: }
677: }
678:
679: public long getOffset() {
680: LOG.info("Perforce server time offset: " + offset + " ms");
681: return offset;
682: }
683: }
684: }
|