001: /********************************************************************************
002: * CruiseControl, a Continuous Integration Toolkit
003: * Copyright (c) 2001-2003, ThoughtWorks, Inc.
004: * 651 W Washington Ave. Suite 600
005: * Chicago, IL 60661 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 com.trinem.harvest.hsdkwrap.JCaConstWrap;
039: import com.trinem.harvest.hsdkwrap.JCaContextWrap;
040: import com.trinem.harvest.hsdkwrap.JCaHarvestWrap;
041: import com.trinem.harvest.hsdkwrap.JCaHarvestLogStreamWrap;
042: import com.trinem.harvest.hsdkwrap.JCaVersionChooserWrap;
043: import com.trinem.harvest.hsdkwrap.IJCaLogStreamListenerImpl;
044: import com.trinem.harvest.hsdkwrap.hutils.JCaAttrKeyWrap;
045: import com.trinem.harvest.hsdkwrap.hutils.JCaContainerWrap;
046: import com.trinem.harvest.hsdkwrap.hutils.JCaHarvestExceptionWrap;
047: import com.trinem.harvest.hsdkwrap.hutils.JCaTimeStampWrap;
048:
049: import net.sourceforge.cruisecontrol.CruiseControlException;
050: import net.sourceforge.cruisecontrol.Modification;
051: import net.sourceforge.cruisecontrol.SourceControl;
052: import net.sourceforge.cruisecontrol.util.ValidationHelper;
053:
054: import org.apache.log4j.Logger;
055:
056: import java.util.ArrayList;
057: import java.util.Calendar;
058: import java.util.Date;
059: import java.util.GregorianCalendar;
060: import java.util.HashMap;
061: import java.util.List;
062: import java.util.Map;
063:
064: /**
065: * SourceControl for CA's AllFusion Harvest Change Manager
066: *
067: * @author <a href="mailto:nayan@chikli.com">Nayan Hajratwala</a>
068: * @author <a href="mailto:info@trinem.com">Trinem Consulting Ltd</a>
069: */
070: public class AllFusionHarvest implements SourceControl {
071:
072: private JCaHarvestWrap harvest = null;
073:
074: private String broker = null;
075: private String username = null;
076: private String password = null;
077:
078: private String project = null;
079: private String state = null;
080:
081: private String property = null;
082: private String propertyOnDelete = null;
083:
084: private Map properties = new HashMap();
085:
086: private boolean loggedIn = false;
087:
088: private static Calendar gc = GregorianCalendar.getInstance();
089: private static Map userEmailMapping = new HashMap();
090:
091: private JCaHarvestLogStreamWrap logstream = null;
092:
093: private static Logger log = Logger
094: .getLogger(AllFusionHarvest.class);
095:
096: /**
097: * Default contructor. Creates a new uninitialise Bootstrapper.
098: */
099: public AllFusionHarvest() {
100: }
101:
102: // ------------------------------------------------------------------------
103: // Property accessors
104: // ------------------------------------------------------------------------
105:
106: /**
107: * Sets the Harvest Broker for all calls to HSDK.
108: *
109: * @param broker
110: * Harvest Broker to use.
111: */
112: public void setBroker(String broker) {
113: log.debug("Broker: " + broker);
114: this .broker = broker;
115: }
116:
117: /**
118: * Sets the Harvest username for all calls to HSDK.
119: *
120: * @param username
121: * Harvest username to use.
122: */
123: public void setUsername(String username) {
124: this .username = username;
125: }
126:
127: /**
128: * Sets the Harvest password for all calls to HSDK.
129: *
130: * @param password
131: * Harvest password to use.
132: */
133: public void setPassword(String password) {
134: this .password = password;
135: }
136:
137: /**
138: * Sets the Harvest project for all calls to HSDK.
139: *
140: * @param project
141: * Harvest project to use.
142: */
143: public void setProject(String project) {
144: this .project = project;
145: }
146:
147: /**
148: * Sets the Harvest state for all calls to HSDK.
149: *
150: * @param state
151: * Harvest state to use.
152: */
153: public void setState(String state) {
154: this .state = state;
155: }
156:
157: /**
158: * Sets the name of the property to set if a modification is detected.
159: *
160: * @param property
161: * The name of the property.
162: */
163: public void setProperty(String property) {
164: this .property = property;
165: }
166:
167: /**
168: * Sets the name of the property to set if a file has been deleted.
169: *
170: * @param propertyOnDelete
171: * The name of the property.
172: */
173: public void setPropertyOnDelete(String propertyOnDelete) {
174: this .propertyOnDelete = propertyOnDelete;
175: }
176:
177: // ------------------------------------------------------------------------
178: // SourceControl implementation methods
179: // ------------------------------------------------------------------------
180:
181: // From SourceControl
182: public Map getProperties() {
183: return properties;
184: }
185:
186: // From SourceControl
187: /**
188: * Standard Bootstrapper validation method. Throws an exception if any of
189: * the required properties are not set.
190: */
191: public void validate() throws CruiseControlException {
192: ValidationHelper.assertIsSet(username, "username", this
193: .getClass());
194: ValidationHelper.assertIsSet(password, "password", this
195: .getClass());
196: ValidationHelper.assertIsSet(broker, "broker", this .getClass());
197: ValidationHelper.assertIsSet(state, "state", this .getClass());
198: ValidationHelper.assertIsSet(project, "project", this
199: .getClass());
200: }
201:
202: /**
203: * Returns a List of Modifications detailing all the changes between the
204: * last build and the latest revision at the repository
205: *
206: * @param lastBuild
207: * last build time
208: * @return maybe empty, never null.
209: */
210: public List getModifications(Date lastBuild, Date now) {
211:
212: log.debug("getModifications( " + lastBuild + ", " + now + " )");
213:
214: if (!login()) {
215: return new ArrayList();
216: }
217:
218: List list = new ArrayList();
219:
220: try {
221: JCaContainerWrap versionList = getVersionsInRange(
222: lastBuild, now);
223:
224: // This test is critical, as sometimes the count throws an exception
225: int numVers = versionList.isEmpty() ? 0 : versionList
226: .getKeyElementCount(JCaAttrKeyWrap.CA_ATTRKEY_NAME);
227:
228: for (int n = 0; n < numVers; n++) {
229: String status = versionList.getString(
230: JCaAttrKeyWrap.CA_ATTRKEY_VERSION_STATUS, n);
231:
232: // Don't add reserved tagged files - the file hasn't actually
233: // changed
234: if (!status.equals("R")) {
235: list
236: .add(transformJCaVersionContainerToModification(
237: versionList, n));
238: }
239: }
240: } catch (JCaHarvestExceptionWrap e) {
241: log.error(e.getMessage());
242: }
243:
244: return list;
245: }
246:
247: // ------------------------------------------------------------------------
248: // Support code
249: // ------------------------------------------------------------------------
250:
251: /**
252: * Returns all the versions that were checked in between two dates.
253: *
254: * @param startDate
255: * the start date
256: * @param endDate
257: * the end date
258: * @return an container of properties representing the versions between the
259: * specified dates
260: * @throws JCaHarvestException
261: */
262: private JCaContainerWrap getVersionsInRange(Date startDate,
263: Date endDate) throws JCaHarvestExceptionWrap {
264:
265: JCaContextWrap context = harvest.getContext();
266:
267: context.setProject(project);
268: context.setState(state);
269:
270: JCaVersionChooserWrap vc = context.getVersionChooser();
271:
272: vc.clear();
273: vc.setRecursive(true);
274: vc.setVersionItemOption(JCaConstWrap.VERSION_FILTER_ITEM_BOTH);
275: vc.setVersionOption(JCaConstWrap.VERSION_FILTER_LATEST_IN_VIEW);
276: vc.setVersionStatusOption(JCaConstWrap.VERSION_FILTER_ALL_TAG);
277: vc.setBranchOption(JCaConstWrap.BRANCH_FILTER_TRUNK_ONLY);
278: vc
279: .setVersionDateOption(JCaConstWrap.VERSION_OPTION_DATE_BETWEEN);
280: vc.setFromDate(convertDateToJCaTimeStamp(startDate));
281: vc.setToDate(convertDateToJCaTimeStamp(endDate));
282:
283: vc.execute();
284:
285: return vc.getVersionList();
286: }
287:
288: /**
289: * Takes a Date object and converts it into a JCaTimeStamp
290: *
291: * @param date
292: * the date to be converted
293: * @return the date as a JCaTimeStamp
294: */
295: private JCaTimeStampWrap convertDateToJCaTimeStamp(Date date) {
296: gc.setTime(date);
297:
298: return new JCaTimeStampWrap(gc.get(Calendar.YEAR), gc
299: .get(Calendar.MONTH)
300: - Calendar.JANUARY + 1, gc.get(Calendar.DAY_OF_MONTH),
301: gc.get(Calendar.HOUR_OF_DAY), gc.get(Calendar.MINUTE),
302: gc.get(Calendar.SECOND), gc.get(Calendar.MILLISECOND));
303: }
304:
305: /**
306: * Transforms a set of version properties into a CruiseControl Modification
307: * object.
308: *
309: * @param versionList
310: * a set of version information properties
311: * @param n
312: * the index of the property information to use
313: * @return a Modification object representing the change
314: */
315: protected Modification transformJCaVersionContainerToModification(
316: JCaContainerWrap versionList, int n) {
317:
318: Modification mod = new Modification("harvest");
319: mod.revision = versionList.getString(
320: JCaAttrKeyWrap.CA_ATTRKEY_MAPPED_VERSION_NAME, n);
321:
322: Modification.ModifiedFile modfile = mod.createModifiedFile(
323: versionList
324: .getString(JCaAttrKeyWrap.CA_ATTRKEY_NAME, n),
325: versionList.getString(
326: JCaAttrKeyWrap.CA_ATTRKEY_FULL_PATH_NAME, n));
327: modfile.revision = mod.revision;
328:
329: JCaTimeStampWrap created = versionList.getTimeStamp(
330: JCaAttrKeyWrap.CA_ATTRKEY_MODIFIED_TIME, n);
331: mod.modifiedTime = created.toDate();
332:
333: mod.userName = versionList.getString(
334: JCaAttrKeyWrap.CA_ATTRKEY_MODIFIER_NAME, n);
335: mod.emailAddress = getEmailAddress(mod.userName);
336: mod.comment = versionList.getString(
337: JCaAttrKeyWrap.CA_ATTRKEY_DESCRIPTION, n);
338:
339: String status = versionList.getString(
340: JCaAttrKeyWrap.CA_ATTRKEY_VERSION_STATUS, n);
341:
342: if (status.equals("N")) {
343: // If this is the first revision, then the file has been newly added
344: if (mod.revision.equals("0")) {
345: modfile.action = "added";
346: } else {
347: modfile.action = "modified";
348: }
349:
350: } else if (status.equals("D")) {
351: modfile.action = "deleted";
352: if (propertyOnDelete != null) {
353: properties.put(propertyOnDelete, "true");
354: }
355: } else if (status.equals("R")) {
356: modfile.action = "reserved";
357: } else if (status.equals("M")) {
358: modfile.action = "merge_tagged";
359: }
360:
361: if (property != null) {
362: properties.put(property, "true");
363: }
364:
365: return mod;
366: }
367:
368: /**
369: * Internal method which connects to Harvest using the details provided.
370: */
371: protected boolean login() {
372: if (loggedIn) {
373: return true;
374: }
375:
376: harvest = new JCaHarvestWrap(broker);
377:
378: logstream = new JCaHarvestLogStreamWrap();
379: logstream.addLogStreamListener(new MyLogStreamListener());
380:
381: harvest.setStaticLog(logstream);
382: harvest.setLog(logstream);
383:
384: if (harvest.login(username, password) != 0) {
385: log.error("Login failed: " + harvest.getLastMessage());
386: return false;
387: }
388:
389: loggedIn = true;
390: return true;
391: }
392:
393: /**
394: * Internal method which disconnects from Harvest.
395: */
396: protected void logout() {
397: try {
398: harvest.logout();
399: loggedIn = false;
400: } catch (JCaHarvestExceptionWrap e) {
401: log.error(e.getMessage());
402: }
403: }
404:
405: /**
406: * Returns an email address for a given username as defined in Harvest.
407: *
408: * @param username
409: * a username
410: * @return the email address corresponding to the username
411: */
412: private String getEmailAddress(String username) {
413:
414: try {
415: String emailAddress = (String) userEmailMapping
416: .get(username);
417:
418: // If we couldn't find the email address, it's probably the first
419: // time we're trying
420: // or it's a new one, so just reload the list.
421: if (emailAddress == null) {
422:
423: if (!login()) {
424: return null;
425: }
426:
427: userEmailMapping.clear();
428: JCaContainerWrap userList = harvest.getUserList();
429: int iNumUsers = userList
430: .getKeyElementCount(JCaAttrKeyWrap.CA_ATTRKEY_NAME);
431: for (int i = 0; i < iNumUsers; i++) {
432: userEmailMapping
433: .put(
434: userList
435: .getString(
436: JCaAttrKeyWrap.CA_ATTRKEY_NAME,
437: i),
438: userList
439: .getString(
440: JCaAttrKeyWrap.CA_ATTRKEY_EMAIL,
441: i));
442: }
443:
444: emailAddress = (String) userEmailMapping.get(username);
445: }
446:
447: return emailAddress;
448: } catch (JCaHarvestExceptionWrap e) {
449: log.error(e.getMessage());
450: }
451:
452: return null;
453: }
454:
455: /**
456: * This is an accessor is only intended to be used for testing. It inserts a
457: * dummy entry into the userEmailMapping table.
458: *
459: * @param username
460: * The name of the user who's email address is being set
461: * @param emailAddress
462: * The corresponding email address of that user
463: */
464: protected void setEmailAddress(String username, String emailAddress) {
465: userEmailMapping.put(username, emailAddress);
466: }
467:
468: /**
469: * This class implements a Harvest log stream listener and takes messages
470: * from Harvest and gives them appropriate log levels in the Log4J stream
471: * for the AllFusionHarvest sourcecontrol. Without this class you would not
472: * see errors from Harvest, nor would warnings and info messages be handled
473: * correctly.
474: *
475: * @author <a href="mailto:info@trinem.com">Trinem Consulting Ltd</a>
476: */
477: public class MyLogStreamListener implements
478: IJCaLogStreamListenerImpl {
479:
480: // From IJCaLogStreamListenerImpl
481: /**
482: * Takes the given message from Harvest, figures out its severity and
483: * reports it back to CruiseControl.
484: *
485: * @param message
486: * The message to process.
487: */
488: public void handleMessage(String message) {
489: int level = JCaHarvestLogStreamWrap
490: .getSeverityLevel(message);
491:
492: // Convert Harvest level to log4j level
493: switch (level) {
494: case JCaHarvestLogStreamWrap.OK:
495: log.debug(message);
496: break;
497: case JCaHarvestLogStreamWrap.INFO:
498: log.info(message);
499: break;
500: case JCaHarvestLogStreamWrap.WARNING:
501: log.warn(message);
502: break;
503: case JCaHarvestLogStreamWrap.ERROR:
504: log.error(message);
505: break;
506: default:
507: }
508: }
509: }
510: }
|