/********************************************************************************
* CruiseControl, a Continuous Integration Toolkit
* Copyright (c) 2001-2003, ThoughtWorks, Inc.
* 651 W Washington Ave. Suite 600
* Chicago, IL 60661 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 com.ca.harvest.jhsdk.JCaConst;
import com.ca.harvest.jhsdk.JCaContext;
import com.ca.harvest.jhsdk.JCaHarvest;
import com.ca.harvest.jhsdk.JCaHarvestLogStream;
import com.ca.harvest.jhsdk.JCaSQL;
import com.ca.harvest.jhsdk.JCaVersionChooser;
import com.ca.harvest.jhsdk.IJCaLogStreamListener;
import com.ca.harvest.jhsdk.hutils.JCaAttrKey;
import com.ca.harvest.jhsdk.hutils.JCaContainer;
import com.ca.harvest.jhsdk.hutils.JCaHarvestException;
import com.ca.harvest.jhsdk.hutils.JCaTimeStamp;
import net.sourceforge.cruisecontrol.CruiseControlException;
import net.sourceforge.cruisecontrol.Modification;
import net.sourceforge.cruisecontrol.SourceControl;
import net.sourceforge.cruisecontrol.util.ValidationHelper;
import org.apache.log4j.Logger;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.TimeZone;
/**
* SourceControl for CA's AllFusion Harvest Change Manager
*
* @author <a href="mailto:nayan@chikli.com">Nayan Hajratwala</a>
* @author <a href="mailto:info@trinem.com">Trinem Consulting Ltd</a>
*/
public class AllFusionHarvest implements SourceControl {
private JCaHarvest harvest = null;
private String broker = null;
private String username = null;
private String password = null;
private String project = null;
private String state = null;
private String prevState = null;
private String viewPath = null;
private int itemOption = JCaConst.VERSION_FILTER_ITEM_BOTH;
private int versionOption = JCaConst.VERSION_FILTER_LATEST_IN_VIEW;
private int statusOption = JCaConst.VERSION_FILTER_ALL_TAG;
private int branchOption = JCaConst.BRANCH_FILTER_TRUNK_ONLY;
public static final int CHECK_VERSION_CHOOSER = 1;
public static final int CHECK_PACKAGE_HISTORY = 2;
private int checkMode = CHECK_VERSION_CHOOSER;
private String property = null;
private String propertyOnDelete = null;
private Map properties = new HashMap();
private boolean loggedIn = false;
private static Calendar gc = GregorianCalendar.getInstance();
private static HashMap userEmailMapping = new HashMap();
private JCaHarvestLogStream logstream = null;
private static final Logger LOG = Logger.getLogger(AllFusionHarvest.class);
private static DateFormat dateFormat = new SimpleDateFormat("MM-dd-yyyy HH:mm:ss");
/**
* Default contructor. Creates a new uninitialise Bootstrapper.
*/
public AllFusionHarvest() {
}
// ------------------------------------------------------------------------
// Property accessors
// ------------------------------------------------------------------------
/**
* Sets the Harvest Broker for all calls to HSDK.
* Required.
*
* @param broker
* Harvest Broker to use.
*/
public void setBroker(String broker) {
LOG.debug("Broker: " + broker);
this.broker = broker;
}
/**
* Sets the Harvest username for all calls to HSDK.
* Required.
*
* @param username
* Harvest username to use.
*/
public void setUsername(String username) {
this.username = username;
}
/**
* Sets the Harvest password for all calls to HSDK.
* Required.
*
* @param password
* Harvest password to use.
*/
public void setPassword(String password) {
this.password = password;
}
/**
* Sets the Harvest project for all calls to HSDK.
* Required.
*
* @param project
* Harvest project to use.
*/
public void setProject(String project) {
this.project = project;
}
/**
* Sets the Harvest state for all calls to HSDK.
* Required.
*
* @param state
* Harvest state to use.
*/
public void setState(String state) {
this.state = state;
}
/**
* Sets the previous Harvest state for package history based monitoring.
* Optional.
*
* @param state
* Harvest state to use.
*/
public void setPrevState(String state) {
this.prevState = state;
}
/**
* Sets the view path to use when making calls to HSDK.
*
* @param viewPath
* String indicating the view path.
*/
public void setViewpath(String viewPath) {
this.viewPath = viewPath;
}
/**
* Sets the version item option to use when making calls to HSDK.
* Optional.
*
* @param itemOption
* String indicating the item option.
*/
public void setItem(String io)
throws CruiseControlException {
if (io.equals("baseline") || io.equals("not_modified")) {
this.itemOption = JCaConst.VERSION_FILTER_ITEM_BASELINE;
} else if (io.equals("modified")) {
this.itemOption = JCaConst.VERSION_FILTER_ITEM_MODIFIED;
} else if (io.equals("both")) {
this.itemOption = JCaConst.VERSION_FILTER_ITEM_BOTH;
} else {
throw new CruiseControlException("item must be one of: "
+ "baseline, modified, (both)");
}
}
/**
* Sets the version item option to use when making calls to HSDK.
* Optional.
*
* @param versionOption
* String indicating the version option.
*/
public void setVersion(String vo)
throws CruiseControlException {
if (vo.equals("latest_in_view")) {
this.versionOption = JCaConst.VERSION_FILTER_LATEST_IN_VIEW;
} else if (vo.equals("all_in_view")) {
this.versionOption = JCaConst.VERSION_FILTER_ALL_IN_VIEW;
} else if (vo.equals("all")) {
this.versionOption = JCaConst.VERSION_FILTER_ALL;
} else if (vo.equals("latest")) {
this.versionOption = JCaConst.VERSION_FILTER_LATEST;
} else {
throw new CruiseControlException("version must be one of: "
+ "(latest_in_view), all_in_view, all, latest");
}
}
/**
* Sets the version status option to use when making calls to HSDK.
* Optional.
*
* @param statusOption
* String indicating the status option.
*/
public void setStatus(String so)
throws CruiseControlException {
if (so.equals("all") || so.equals("all_tags")) {
this.statusOption = JCaConst.VERSION_FILTER_ALL_TAG;
} else if (so.equals("no_tag") || so.equals("normal")) {
this.statusOption = JCaConst.VERSION_FILTER_NORMAL_VERSION;
} else if (so.equals("reserved")) {
this.statusOption = JCaConst.VERSION_FILTER_RESERVED_VERSION;
} else if (so.equals("merged")) {
this.statusOption = JCaConst.VERSION_FILTER_MERGED_VERSION;
} else if (so.equals("removed") || so.equals("deleted")) {
this.statusOption = JCaConst.VERSION_FILTER_DELETED_VERSION;
} else if (so.equals("any") || so.equals("any_tag")) {
this.statusOption = JCaConst.VERSION_FILTER_ANY_TAG;
} else {
throw new CruiseControlException("status must be one of: "
+ "(all), no_tag, reserved, merged, removed, any");
}
}
/**
* Sets the version branch option to use when making calls to HSDK.
* Optional.
*
* @param branchOption
* String indicating the branch option.
*/
public void setBranch(String bo)
throws CruiseControlException {
/* Also: BRANCH_FILTER_MERGED_ONLY, BRANCH_FILTER_VCI_ONLY? */
if (bo.equals("trunk") || bo.equals("trunk_only")) {
this.branchOption = JCaConst.BRANCH_FILTER_TRUNK_ONLY;
} else if (bo.equals("branch") || bo.equals("branch_only")) {
this.branchOption = JCaConst.BRANCH_FILTER_BRANCH_ONLY;
} else if (bo.equals("trunk_and_branch")) {
this.branchOption = JCaConst.BRANCH_FILTER_TRUNK_AND_BRANCH;
} else if (bo.equals("unmerged") || bo.equals("unmerged_branch")) {
this.branchOption = JCaConst.BRANCH_FILTER_UNMERGED_ONLY;
} else {
throw new CruiseControlException("branch must be one of: "
+ "(trunk), branch, trunk_and_branch, unmerged");
}
}
/**
* Sets the mode used to check for modifications. In version mode, the
* version chooser is used to find versions visible in the selected state
* which have been created after the last build occurred. In package mode,
* the package history is checked to find packages which have been promoted
* or demoted into the selected state since the last build occurred.
* Optional.
*
* @param mode
* String indicating the mode to use.
*/
public void setMode(String mode)
throws CruiseControlException {
if (mode.equals("version")) {
this.checkMode = CHECK_VERSION_CHOOSER;
} else if (mode.equals("package")) {
this.checkMode = CHECK_PACKAGE_HISTORY;
} else {
throw new CruiseControlException("mode must be one of: "
+ "(version), package");
}
}
/**
* Sets the name of the property to set if a modification is detected.
*
* @param property
* The name of the property.
*/
public void setProperty(String property) {
this.property = property;
}
/**
* Sets the name of the property to set if a file has been deleted.
*
* @param propertyOnDelete
* The name of the property.
*/
public void setPropertyOnDelete(String propertyOnDelete) {
this.propertyOnDelete = propertyOnDelete;
}
// ------------------------------------------------------------------------
// SourceControl implementation methods
// ------------------------------------------------------------------------
// From SourceControl
public Map getProperties() {
return properties;
}
// From SourceControl
/**
* Standard Bootstrapper validation method. Throws an exception if any of
* the required properties are not set.
*/
public void validate() throws CruiseControlException {
ValidationHelper.assertIsSet(username, "username", this.getClass());
ValidationHelper.assertIsSet(password, "password", this.getClass());
ValidationHelper.assertIsSet(broker, "broker", this.getClass());
ValidationHelper.assertIsSet(state, "state", this.getClass());
ValidationHelper.assertIsSet(project, "project", this.getClass());
}
/**
* Returns a List of Modifications detailing all the changes between the
* last build and the latest revision at the repository
*
* @param lastBuild
* last build time
* @return maybe empty, never null.
*/
public List getModifications(Date lastBuild, Date now) {
LOG.debug("getModifications( " + lastBuild + ", " + now + " )");
if (!login()) {
return new ArrayList();
}
List list = new ArrayList();
JCaContainer versionList = null;
JCaContainer demotedVersionList = null;
try {
if (checkMode == CHECK_VERSION_CHOOSER) {
versionList = getVersionsInRange(lastBuild, now);
} else {
versionList = getPromotedAndDemotedVersions(lastBuild, true);
if (prevState != null) {
demotedVersionList = getPromotedAndDemotedVersions(lastBuild, false);
}
}
// This test is critical, as sometimes the count throws an exception
int numVers = (versionList == null) || versionList.isEmpty() ? 0
: versionList.getKeyElementCount(JCaAttrKey.CA_ATTRKEY_NAME);
for (int n = 0; n < numVers; n++) {
String status = versionList.getString(JCaAttrKey.CA_ATTRKEY_VERSION_STATUS, n);
// Don't add reserved tagged files - the file hasn't actually changed
if (!status.equals("R")) {
list.add(transformJCaVersionContainerToModification(versionList, n, true));
}
}
// This test is critical, as sometimes the count throws an exception
numVers = (demotedVersionList == null) || demotedVersionList.isEmpty() ? 0
: demotedVersionList.getKeyElementCount(JCaAttrKey.CA_ATTRKEY_NAME);
for (int n = 0; n < numVers; n++) {
String status = demotedVersionList.getString(JCaAttrKey.CA_ATTRKEY_VERSION_STATUS, n);
// Don't add reserved tagged files - the file hasn't actually
// changed
if (!status.equals("R")) {
list.add(transformJCaVersionContainerToModification(demotedVersionList, n, false));
}
}
} catch (JCaHarvestException e) {
LOG.error(e.getMessage());
}
return list;
}
// ------------------------------------------------------------------------
// Support code
// ------------------------------------------------------------------------
/**
* Returns all the versions that were checked in between two dates.
*
* @param startDate
* the start date
* @param endDate
* the end date
* @return an container of properties representing the versions between the
* specified dates
* @throws JCaHarvestException
*/
private JCaContainer getVersionsInRange(Date startDate, Date endDate) throws JCaHarvestException {
JCaContext context = harvest.getContext();
context.setProject(project);
context.setState(state);
JCaVersionChooser vc = context.getVersionChooser();
vc.clear();
vc.setRecursive(true);
if (viewPath != null) {
vc.setParentPath(viewPath);
}
vc.setVersionItemOption(itemOption); // Defaults to JCaConst.VERSION_FILTER_ITEM_BOTH
vc.setVersionOption(versionOption); // Defaults to JCaConst.VERSION_FILTER_LATEST_IN_VIEW
vc.setVersionStatusOption(statusOption); // Defaults to JCaConst.VERSION_FILTER_ALL_TAG
vc.setBranchOption(branchOption); // Defaults to JCaConst.BRANCH_FILTER_TRUNK_ONLY
vc.setVersionDateOption(JCaConst.VERSION_OPTION_DATE_BETWEEN);
vc.setFromDate(convertDateToJCaTimeStamp(startDate));
vc.setToDate(convertDateToJCaTimeStamp(endDate));
vc.execute();
return vc.getVersionList();
}
/**
* Returns all the versions that were promoted or demoted after the given date.
*
* @param startDate
* the start date
* @return an container of properties representing the versions
* @throws JCaHarvestException
*/
private JCaContainer getPromotedAndDemotedVersions(Date startDate, boolean promote)
throws JCaHarvestException {
JCaContext context = harvest.getContext();
context.setProject(project);
context.setState(state);
int[] pkgobjids = null;
JCaSQL sql = context.getSQL();
if (promote) {
sql.setSQLStatement("SELECT DISTINCT packageObjId FROM harPkgHistory"
+ " WHERE environmentName='" + project + "'"
+ " AND stateName='" + state + "'"
+ " AND action='Promote'"
+ " AND execdTime > '" + convertDateToDatabaseDate(startDate) + "'");
} else {
if (prevState.indexOf(",") != -1) {
StringTokenizer tok = new StringTokenizer(prevState, ",");
StringBuffer buf = new StringBuffer();
while (tok.hasMoreTokens()) {
if (buf.length() > 0) {
buf.append("','");
}
buf.append(tok.nextToken());
}
prevState = buf.toString();
}
sql.setSQLStatement(" SELECT DISTINCT packageObjId FROM harPkgHistory"
+ " WHERE environmentName='" + project + "'"
+ " AND stateName IN ('" + prevState + "')"
+ " AND action='Demote'"
+ " AND execdTime > '" + convertDateToDatabaseDate(startDate) + "'");
}
sql.execute();
JCaContainer sqlData = sql.getSQLResult();
if (sqlData.getKeyCount() > 0) {
int count = sqlData.getKeyElementCount("PACKAGEOBJID");
if (count > 0) {
pkgobjids = new int[count];
for (int n = 0; n < count; n++) {
pkgobjids[n] = sqlData.getInt("PACKAGEOBJID", n);
LOG.info("Package " + pkgobjids[n] + " has been "
+ (promote ? "promoted" : "demoted"));
}
}
}
if ((pkgobjids == null) || (pkgobjids.length == 0)) {
return new JCaContainer();
}
JCaVersionChooser vc = context.getVersionChooser();
vc.clear();
vc.setRecursive(true);
if (viewPath != null) {
vc.setParentPath(viewPath);
}
vc.setVersionItemOption(itemOption); // Defaults to JCaConst.VERSION_FILTER_ITEM_BOTH
vc.setVersionOption(versionOption); // Defaults to JCaConst.VERSION_FILTER_LATEST_IN_VIEW
vc.setVersionStatusOption(statusOption); // Defaults to JCaConst.VERSION_FILTER_ALL_TAG
vc.setBranchOption(branchOption); // Defaults to JCaConst.BRANCH_FILTER_TRUNK_ONLY
for (int n = 0; n < pkgobjids.length; n++) {
vc.setPackageObjId(pkgobjids[n], n);
}
vc.execute();
return vc.getVersionList();
}
/**
* Takes a Date object and converts it into a JCaTimeStamp
*
* @param date
* the date to be converted
* @return the date as a JCaTimeStamp
*/
private JCaTimeStamp convertDateToJCaTimeStamp(Date date) {
gc.setTime(date);
return new JCaTimeStamp(gc.get(Calendar.YEAR),
gc.get(Calendar.MONTH) - Calendar.JANUARY + 1,
gc.get(Calendar.DAY_OF_MONTH),
gc.get(Calendar.HOUR_OF_DAY),
gc.get(Calendar.MINUTE),
gc.get(Calendar.SECOND),
gc.get(Calendar.MILLISECOND));
}
/**
* Takes a Date object and converts it into a String suitable for use in
* database queries. As the database stores dates in UTC, we must also
* convert the passed date to UTC, otherwise we may be an hour out due to
* daylight savings times.
*
* @param date
* the date to be converted
* @return the date as a String
*/
private String convertDateToDatabaseDate(Date date) {
dateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
return dateFormat.format(date);
}
/**
* Transforms a set of version properties into a CruiseControl Modification
* object.
*
* @param versionList
* a set of version information properties
* @param n
* the index of the property information to use
* @return a Modification object representing the change
*/
protected Modification transformJCaVersionContainerToModification(
JCaContainer versionList, int n, boolean promote) {
Modification mod = new Modification("harvest");
mod.revision = versionList.getString(JCaAttrKey.CA_ATTRKEY_MAPPED_VERSION_NAME, n);
Modification.ModifiedFile modfile = mod.createModifiedFile(
versionList.getString(JCaAttrKey.CA_ATTRKEY_NAME, n),
versionList.getString(JCaAttrKey.CA_ATTRKEY_FULL_PATH_NAME, n));
modfile.revision = mod.revision;
JCaTimeStamp created = versionList.getTimeStamp(JCaAttrKey.CA_ATTRKEY_MODIFIED_TIME, n);
mod.modifiedTime = created.toDate();
mod.userName = versionList.getString(JCaAttrKey.CA_ATTRKEY_MODIFIER_NAME, n);
mod.emailAddress = getEmailAddress(mod.userName);
mod.comment = versionList.getString(JCaAttrKey.CA_ATTRKEY_DESCRIPTION, n);
String status = versionList.getString(JCaAttrKey.CA_ATTRKEY_VERSION_STATUS, n);
if (promote) {
if (status.equals("N")) {
// If this is the first revision, then the file has been newly added
if (mod.revision.equals("0")) {
modfile.action = "added";
} else {
modfile.action = "modified";
}
} else if (status.equals("D")) {
modfile.action = "deleted";
if (propertyOnDelete != null) {
properties.put(propertyOnDelete, "true");
}
} else if (status.equals("R")) {
modfile.action = "reserved";
} else if (status.equals("M")) {
modfile.action = "merge_tagged";
}
} else {
// Any versions which have been demoted are effectively deleted from the state
modfile.action = "deleted";
if (propertyOnDelete != null) {
properties.put(propertyOnDelete, "true");
}
}
if (property != null) {
properties.put(property, "true");
}
return mod;
}
/**
* Internal method which connects to Harvest using the details provided.
*/
protected boolean login() {
if (loggedIn) {
return true;
}
harvest = new JCaHarvest(broker);
logstream = new JCaHarvestLogStream();
logstream.addLogStreamListener(new MyLogStreamListener());
harvest.setStaticLog(logstream);
harvest.setLog(logstream);
if (harvest.login(username, password) != 0) {
LOG.error("Login failed: " + harvest.getLastMessage());
return false;
}
loggedIn = true;
return true;
}
/**
* Internal method which disconnects from Harvest.
*/
protected void logout() {
try {
harvest.logout();
loggedIn = false;
} catch (JCaHarvestException e) {
LOG.error(e.getMessage());
}
}
/**
* Returns an email address for a given username as defined in Harvest.
*
* @param username
* a username
* @return the email address corresponding to the username
*/
private String getEmailAddress(String username) {
try {
String emailAddress = (String) userEmailMapping.get(username);
// If we couldn't find the email address, it's probably the first
// time we're trying
// or it's a new one, so just reload the list.
if (emailAddress == null) {
if (!login()) {
return null;
}
userEmailMapping.clear();
JCaContainer userList = harvest.getUserList();
int iNumUsers = userList.getKeyElementCount(JCaAttrKey.CA_ATTRKEY_NAME);
for (int i = 0; i < iNumUsers; i++) {
userEmailMapping.put(userList.getString(JCaAttrKey.CA_ATTRKEY_NAME, i),
userList.getString(JCaAttrKey.CA_ATTRKEY_EMAIL, i));
}
emailAddress = (String) userEmailMapping.get(username);
}
return emailAddress;
} catch (JCaHarvestException e) {
LOG.error(e.getMessage());
}
return null;
}
/**
* This is an accessor is only intended to be used for testing. It inserts a
* dummy entry into the userEmailMapping table.
*
* @param username
* The name of the user who's email address is being set
* @param emailAddress
* The corresponding email address of that user
*/
protected void setEmailAddress(String username, String emailAddress) {
userEmailMapping.put(username, emailAddress);
}
/**
* This class implements a Harvest LOG stream listener and takes messages
* from Harvest and gives them appropriate LOG levels in the Log4J stream
* for the AllFusionHarvest sourcecontrol. Without this class you would not
* see errors from Harvest, nor would warnings and info messages be handled
* correctly.
*
* @author <a href="mailto:info@trinem.com">Trinem Consulting Ltd</a>
*/
public class MyLogStreamListener implements IJCaLogStreamListener {
// From IJCaLogStreamListener
/**
* Takes the given message from Harvest, figures out its severity and
* reports it back to CruiseControl.
*
* @param message
* The message to process.
*/
public void handleMessage(String message) {
int level = JCaHarvestLogStream.getSeverityLevel(message);
// Convert Harvest level to log4j level
switch (level) {
case JCaHarvestLogStream.INFO:
LOG.info(message);
break;
case JCaHarvestLogStream.WARNING:
LOG.warn(message);
break;
case JCaHarvestLogStream.ERROR:
LOG.error(message);
break;
case JCaHarvestLogStream.OK:
default:
LOG.debug(message);
break;
}
}
}
}