/********************************************************************************
* CruiseControl, a Continuous Integration Toolkit
* Copyright (c) 2001-2003, ThoughtWorks, Inc.
* 200 E. Randolph, 25th Floor
* Chicago, IL 60601 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;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.management.JMException;
import javax.management.MBeanServer;
import net.sourceforge.cruisecontrol.events.BuildProgressEvent;
import net.sourceforge.cruisecontrol.events.BuildProgressListener;
import net.sourceforge.cruisecontrol.events.BuildResultEvent;
import net.sourceforge.cruisecontrol.events.BuildResultListener;
import net.sourceforge.cruisecontrol.jmx.ProjectController;
import net.sourceforge.cruisecontrol.listeners.ProjectStateChangedEvent;
import net.sourceforge.cruisecontrol.util.CVSDateUtil;
import net.sourceforge.cruisecontrol.util.DateUtil;
import net.sourceforge.cruisecontrol.util.Util;
import net.sourceforge.cruisecontrol.util.XMLLogHelper;
import org.apache.log4j.Logger;
import org.jdom.Element;
/**
* Represents a single logical project consisting of source code that needs to
* be built. Project is associated with bootstrappers that run before builds
* and a Schedule that determines when builds occur.
*/
public class Project implements Serializable, Runnable, ProjectQuery {
private static final long serialVersionUID = 2656877748476842326L;
private static final Logger LOG = Logger.getLogger(Project.class);
private transient ProjectState state;
private transient ProjectConfig projectConfig;
private transient LabelIncrementer labelIncrementer;
/**
* If this attribute is set, then it means that the user has overridden
* the build interval specified in the Schedule element, probably
* using the JMX interface.
*/
private transient Long overrideBuildInterval;
private transient Date buildStartTime;
private transient Object pausedMutex;
private transient Object scheduleMutex;
private transient Object waitMutex;
private transient BuildQueue queue;
private transient List<BuildProgressListener> progressListeners;
private transient List<BuildResultListener> resultListeners;
private transient Progress progress;
private int buildCounter = 0;
private Date lastBuild = DateUtil.getMidnight();
private Date lastSuccessfulBuild = lastBuild;
private boolean wasLastBuildSuccessful = true;
private String label;
private String name;
private transient boolean buildForced = false;
private String buildTarget = null;
private boolean isPaused = false;
private boolean buildAfterFailed = true;
private boolean stopped = true;
private boolean forceOnly = false;
private boolean requiremodification = true;
private Map<String, String> additionalProperties;
public Project() {
initializeTransientFields();
}
private void initializeTransientFields() {
state = ProjectState.STOPPED;
pausedMutex = new Object();
scheduleMutex = new Object();
waitMutex = new Object();
progressListeners = new ArrayList<BuildProgressListener>();
resultListeners = new ArrayList<BuildResultListener>();
progress = new ProgressImpl(this);
}
private void readObject(final ObjectInputStream stream) throws IOException, ClassNotFoundException {
stream.defaultReadObject();
initializeTransientFields();
}
public void execute() {
if (stopped) {
LOG.warn("not building project " + name + " because project has been stopped.");
buildFinished();
return;
}
synchronized (pausedMutex) {
if (isPaused) {
LOG.info("not building project " + name + " because project has been paused.");
buildFinished();
return;
}
}
try {
init();
build();
} catch (CruiseControlException e) {
LOG.error("exception attempting build in project " + name, e);
} finally {
buildFinished();
}
}
/**
* Unless paused, runs any bootstrappers and then the entire build.
* @throws CruiseControlException if an error occurs during the build
*/
protected void build() throws CruiseControlException {
if (projectConfig == null) {
throw new IllegalStateException("projectConfig must be set on project before calling build()");
}
if (stopped) {
LOG.warn("not building project " + name + " because project has been stopped.");
return;
}
// If the force only flag is set, only build if forced
// or if the last build faild and we want to build on failures
if (forceOnly && !buildForced && !(!wasLastBuildSuccessful && buildAfterFailed)) {
info("not building because project is forceOnly and build not forced.");
return;
}
final boolean buildWasForced = buildForced;
try {
setBuildStartTime(new Date());
final Schedule schedule = projectConfig.getSchedule();
if (schedule == null) {
throw new IllegalStateException("project must have a schedule");
}
if (schedule.isPaused(buildStartTime)) {
// a regularly scheduled paused
// is different than ProjectState.PAUSED
return;
}
// @todo Add Progress param to Bootstrapper API?
bootstrap();
final String target = useAndResetBuildTargetIfBuildWasForced(buildWasForced);
// @todo Add Progress param to ModificationSet API?
// getModifications will only return null if we don't need to build
final Element modifications = getModifications(buildWasForced);
if (modifications == null) {
return;
}
// Using local reference to avoid NPE if config.xml is updated during build
final Log buildLog = projectConfig.getLog();
buildLog.addContent(modifications);
final Date now;
if (projectConfig.getModificationSet() != null
&& projectConfig.getModificationSet().getTimeOfCheck() != null) {
now = projectConfig.getModificationSet().getTimeOfCheck();
} else {
now = new Date();
}
if (getLabelIncrementer().isPreBuildIncrementer()) {
label = getLabelIncrementer().incrementLabel(label, buildLog.getContent());
}
// collect project information
buildLog.addContent(getProjectPropertiesElement(now));
setState(ProjectState.BUILDING);
final Element builderLog = schedule.build(buildCounter, lastBuild, now, getProjectPropertiesMap(now),
target, progress);
buildLog.addContent(builderLog.detach());
boolean buildSuccessful = buildLog.wasBuildSuccessful();
fireResultEvent(new BuildResultEvent(this, buildSuccessful));
if (!getLabelIncrementer().isPreBuildIncrementer() && buildSuccessful) {
label = getLabelIncrementer().incrementLabel(label, buildLog.getContent());
}
setState(ProjectState.MERGING_LOGS);
buildLog.writeLogFile(now);
// If we only want to build after a check in, even when broken, set the last build to now,
// regardless of success or failure (buildAfterFailed = false in config.xml)
if (!buildAfterFailed) {
lastBuild = now;
}
// If this was a successful build, update both last build and last successful build
if (buildSuccessful) {
lastBuild = now;
lastSuccessfulBuild = now;
info("build successful");
} else {
info("build failed");
}
buildCounter++;
setWasLastBuildSuccessful(buildSuccessful);
// also need to reset forced flag before serializing, unless buildForced var is transient
//resetBuildForcedOnlyIfBuildWasForced(buildWasForced);
serializeProject();
// @todo Add Progress param to Publisher API?
publish(buildLog);
buildLog.reset();
} finally {
resetBuildForcedOnlyIfBuildWasForced(buildWasForced);
setState(ProjectState.IDLE);
}
}
private String useAndResetBuildTargetIfBuildWasForced(final boolean buildWasForced) {
String target = null;
if (buildWasForced) {
target = buildTarget;
buildTarget = null;
}
return target;
}
private void resetBuildForcedOnlyIfBuildWasForced(final boolean buildWasForced) {
if (buildWasForced) {
buildForced = false;
}
}
void setBuildStartTime(final Date date) {
buildStartTime = date;
}
public void run() {
LOG.info("Project " + name + " started");
try {
while (!stopped) {
try {
waitIfPaused();
if (!stopped) {
waitForNextBuild();
}
if (!stopped) {
setState(ProjectState.QUEUED);
synchronized (scheduleMutex) {
queue.requestBuild(projectConfig);
waitForBuildToFinish();
}
}
} catch (InterruptedException e) {
final String message = "Project " + name + ".run() interrupted";
LOG.error(message, e);
throw new RuntimeException(message);
}
}
} finally {
stopped = true;
LOG.info("Project " + name + " stopped");
}
}
void waitIfPaused() throws InterruptedException {
synchronized (pausedMutex) {
while (isPaused) {
setState(ProjectState.PAUSED);
pausedMutex.wait(10 * DateUtil.ONE_MINUTE);
}
}
}
void waitForNextBuild() throws InterruptedException {
long waitTime = getTimeToNextBuild(new Date());
if (needToWaitForNextBuild(waitTime) && !buildForced) {
final String msg = "next build in " + DateUtil.formatTime(waitTime);
info(msg);
synchronized (waitMutex) {
setState(ProjectState.WAITING);
progress.setValue(msg);
waitMutex.wait(waitTime);
}
}
}
long getTimeToNextBuild(Date now) {
long waitTime = projectConfig.getSchedule().getTimeToNextBuild(now, getBuildInterval());
if (waitTime == 0) {
// check for the exceptional case that we're dealing with a
// project that has just built within a minute time
if (buildStartTime != null) {
long millisSinceLastBuild = now.getTime() - buildStartTime.getTime();
if (millisSinceLastBuild < Schedule.ONE_MINUTE) {
debug("build finished within a minute, getting new time to next build");
Date oneMinuteInFuture = new Date(now.getTime() + Schedule.ONE_MINUTE);
waitTime = projectConfig.getSchedule().getTimeToNextBuild(oneMinuteInFuture, getBuildInterval());
waitTime += Schedule.ONE_MINUTE;
}
}
}
return waitTime;
}
static boolean needToWaitForNextBuild(long waitTime) {
return waitTime > 0;
}
/** @return true if build was forced, intended for unit testing only. */
boolean isBuildForced() {
return buildForced;
}
void forceBuild() {
synchronized (waitMutex) {
waitMutex.notify();
}
}
public void forceBuildWithTarget(String buildTarget) {
this.buildTarget = buildTarget;
setBuildForced(true);
}
public void forceBuildWithTarget(String buildTarget, Map<String, String> addedProperties) {
additionalProperties = addedProperties;
forceBuildWithTarget(buildTarget);
}
void waitForBuildToFinish() throws InterruptedException {
synchronized (scheduleMutex) {
debug("waiting for build to finish");
scheduleMutex.wait();
}
}
void buildFinished() {
synchronized (scheduleMutex) {
debug("build finished");
scheduleMutex.notify();
}
}
/**
* Return modifications since the last build. timeOfCheck will be updated according to the last modification to
* account for time synchronisation issues.
*
* @param buildWasForced true if the build was forced
* @return Element jdom element containing modification information
*/
Element getModifications(final boolean buildWasForced) {
setState(ProjectState.MODIFICATIONSET);
final ModificationSet modificationSet = projectConfig.getModificationSet();
if (modificationSet == null) {
debug("no modification set, nothing to detect.");
if (buildWasForced) {
info("no modification set but build was forced");
return new Element("modifications");
}
if (!requiremodification) {
info("no modification set but no modifications required");
return new Element("modifications");
}
return null;
}
final boolean checkNewChangesFirst = checkOnlySinceLastBuild();
Element modifications;
if (checkNewChangesFirst) {
debug("getting changes since last build");
modifications = modificationSet.retrieveModificationsAsElement(lastBuild, progress);
} else {
debug("getting changes since last successful build");
modifications = modificationSet.retrieveModificationsAsElement(lastSuccessfulBuild, progress);
}
if (!modificationSet.isModified()) {
info("No modifications found, build not necessary.");
// Sometimes we want to build even though we don't have any
// modifications:
// * last build failed & buildaferfailed="true"
// * requiremodifications="false"
// * build forced
if (buildAfterFailed && !wasLastBuildSuccessful) {
info("Building anyway, since buildAfterFailed is true and last build failed.");
} else if (!requiremodification) {
info("Building anyway, since modifications not required");
} else {
if (buildWasForced) {
info("Building anyway, since build was explicitly forced.");
} else {
return null;
}
}
}
if (checkNewChangesFirst) {
debug("new changes found; now getting complete set");
modifications = modificationSet.retrieveModificationsAsElement(lastSuccessfulBuild, progress);
}
return modifications;
}
/**
* @return boolean
*/
boolean checkOnlySinceLastBuild() {
if (lastBuild == null || lastSuccessfulBuild == null) {
return false;
}
final long lastBuildLong = lastBuild.getTime();
final long timeDifference = lastBuildLong - lastSuccessfulBuild.getTime();
final boolean moreThanASecond = timeDifference > DateUtil.ONE_SECOND;
return !buildAfterFailed && moreThanASecond;
}
/**
* Serialize the project to allow resumption after a process bounce
*/
public void serializeProject() {
final String safeProjectName = Builder.getFileSystemSafeProjectName(name);
try {
final ObjectOutputStream s = new ObjectOutputStream(new FileOutputStream(safeProjectName + ".ser"));
try {
s.writeObject(this);
s.flush();
debug("Serializing project to [" + safeProjectName + ".ser]");
} finally {
s.close();
}
} catch (Exception e) {
LOG.warn("Error serializing project to [" + safeProjectName + ".ser]: "
+ e.getMessage(), e);
}
}
public void setLabelIncrementer(final LabelIncrementer incrementer) throws CruiseControlException {
if (incrementer == null) {
throw new IllegalArgumentException("label incrementer can't be null");
}
labelIncrementer = incrementer;
if (label == null) {
label = labelIncrementer.getDefaultLabel();
}
validateLabel(label, labelIncrementer);
}
public LabelIncrementer getLabelIncrementer() {
return labelIncrementer;
}
public void setName(final String projectName) {
name = projectName;
}
public String getName() {
return name;
}
public void setLabel(final String newLabel) {
label = newLabel;
}
public String getLabel() {
return label;
}
/**
* @param newLastBuild string containing the build date in the format
* yyyyMMddHHmmss
* @throws CruiseControlException if the date cannot be extracted from the
* input string
*/
public void setLastBuild(final String newLastBuild) throws CruiseControlException {
lastBuild = DateUtil.parseFormattedTime(newLastBuild, "lastBuild");
}
/**
* @param newLastSuccessfulBuild string containing the build date in the format
* yyyyMMddHHmmss
* @throws CruiseControlException if the date cannot be extracted from the
* input string
*/
public void setLastSuccessfulBuild(final String newLastSuccessfulBuild)
throws CruiseControlException {
lastSuccessfulBuild = DateUtil.parseFormattedTime(newLastSuccessfulBuild, "lastSuccessfulBuild");
}
public String getLastBuild() {
if (lastBuild == null) {
return null;
}
return DateUtil.getFormattedTime(lastBuild);
}
public boolean getBuildForced() {
return buildForced;
}
public void setBuildForced(boolean forceNewBuildNow) {
buildForced = forceNewBuildNow;
if (forceNewBuildNow) {
forceBuild();
}
}
public String getLastSuccessfulBuild() {
if (lastSuccessfulBuild == null) {
return null;
}
return DateUtil.getFormattedTime(lastSuccessfulBuild);
}
public String getLogDir() {
return projectConfig.getLog().getLogDir();
}
/**
* Returns the build interval. This value is initially specified on the
* schedule, but the user may override that value using the JMX interface.
* If the user hasn't override the Schedule, then this method will
* return the Schedule's interval, otherwise the overridden value will
* be returned.
* @return the build interval
*/
public long getBuildInterval() {
if (overrideBuildInterval == null) {
return projectConfig.getSchedule().getInterval();
} else {
return overrideBuildInterval;
}
}
/**
* Sets the build interval that this Project should use. This method
* overrides the value initially specified in the Schedule attribute.
* @param sleepMillis the number of milliseconds to sleep between build attempts
*/
public void overrideBuildInterval(final long sleepMillis) {
overrideBuildInterval = sleepMillis;
}
public boolean isPaused() {
return isPaused;
}
public void setPaused(final boolean paused) {
synchronized (pausedMutex) {
if (isPaused && !paused) {
pausedMutex.notifyAll();
}
isPaused = paused;
}
}
public void setBuildAfterFailed(final boolean rebuildEvenWithNoNewModifications) {
buildAfterFailed = rebuildEvenWithNoNewModifications;
}
public String getStatus() {
return getState().getDescription();
}
public String getStatusWithQueuePosition() {
if (ProjectState.QUEUED.equals(getState())) {
return getState().getDescription() + " - " + queue.findPosition(projectConfig);
} else {
return getState().getDescription();
}
}
public ProjectState getState() {
return state;
}
private void setState(final ProjectState newState) {
state = newState;
info(getStatus());
notifyListeners(new ProjectStateChangedEvent(name, getState()));
fireProgressEvent(new BuildProgressEvent(this, getState()));
}
public void setBuildQueue(final BuildQueue buildQueue) {
queue = buildQueue;
}
public String getBuildStartTime() {
return DateUtil.getFormattedTime(buildStartTime);
}
public Log getLog() {
return this.projectConfig.getLog();
}
/**
* Initialize the project. Uses ProjectXMLHelper to parse a project file.
*/
protected void init() {
if (projectConfig == null) {
throw new IllegalStateException("projectConfig must be set on project before calling init()");
}
buildAfterFailed = projectConfig.shouldBuildAfterFailed();
forceOnly = projectConfig.isForceOnly();
requiremodification = projectConfig.isRequiremodification();
if (lastBuild == null) {
lastBuild = DateUtil.getMidnight();
}
if (lastSuccessfulBuild == null) {
lastSuccessfulBuild = lastBuild;
}
if (LOG.isDebugEnabled()) {
debug("buildInterval = [" + getBuildInterval() + "]");
debug("buildForced = [" + buildForced + "]");
debug("buildAfterFailed = [" + buildAfterFailed + "]");
debug("requireModifcation = [" + requiremodification + "]");
debug("forceOnly = [" + forceOnly + "]");
debug("buildCounter = [" + buildCounter + "]");
debug("isPaused = [" + isPaused + "]");
debug("label = [" + label + "]");
debug("lastBuild = [" + DateUtil.getFormattedTime(lastBuild) + "]");
debug("lastSuccessfulBuild = [" + DateUtil.getFormattedTime(lastSuccessfulBuild) + "]");
debug("logDir = [" + projectConfig.getLog().getLogDir() + "]");
debug("logXmlEncoding = [" + projectConfig.getLog().getLogXmlEncoding() + "]");
debug("wasLastBuildSuccessful = [" + wasLastBuildSuccessful + "]");
}
}
protected Element getProjectPropertiesElement(final Date now) {
final Element infoElement = new Element("info");
addProperty(infoElement, "projectname", name);
final String lastBuildString = DateUtil.getFormattedTime(lastBuild == null ? now : lastBuild);
addProperty(infoElement, "lastbuild", lastBuildString);
final String lastSuccessfulBuildString =
DateUtil.getFormattedTime(lastSuccessfulBuild == null ? now : lastSuccessfulBuild);
addProperty(infoElement, "lastsuccessfulbuild", lastSuccessfulBuildString);
addProperty(infoElement, "builddate", DateUtil.formatIso8601(now));
addProperty(infoElement, "cctimestamp", DateUtil.getFormattedTime(now));
addProperty(infoElement, "label", label);
addProperty(infoElement, "interval", Long.toString(getBuildInterval() / 1000L));
addProperty(infoElement, "lastbuildsuccessful", String.valueOf(wasLastBuildSuccessful));
return infoElement;
}
private void addProperty(final Element parent, final String key, final String value) {
final Element propertyElement = new Element("property");
propertyElement.setAttribute("name", key);
propertyElement.setAttribute("value", value);
parent.addContent(propertyElement);
}
protected Map<String, String> getProjectPropertiesMap(final Date now) {
final Map<String, String> buildProperties = new HashMap<String, String>();
buildProperties.put("projectname", name);
buildProperties.put("label", label);
// TODO: Shouldn't have CVS specific properties here
buildProperties.put("cvstimestamp", CVSDateUtil.formatCVSDate(now));
buildProperties.put("cctimestamp", DateUtil.getFormattedTime(now));
buildProperties.put("cclastgoodbuildtimestamp", getLastSuccessfulBuild());
buildProperties.put("cclastbuildtimestamp", getLastBuild());
buildProperties.put("lastbuildsuccessful", String.valueOf(isLastBuildSuccessful()));
buildProperties.put("buildforced", String.valueOf(getBuildForced()));
if (projectConfig.getModificationSet() != null) {
buildProperties.putAll(projectConfig.getModificationSet().getProperties());
}
if (additionalProperties != null && !additionalProperties.isEmpty()) {
buildProperties.putAll(additionalProperties);
additionalProperties.clear();
additionalProperties = null;
}
return buildProperties;
}
/**
* Intended only for unit testing.
* @return additional Properties variable.
*/
Map<String, String> getAdditionalProperties() {
return additionalProperties;
}
/**
* Iterate over all of the registered <code>Publisher</code>s and call
* their respective <code>publish</code> methods.
* @param buildLog the content to publish
* @throws CruiseControlException if an error occurs during publishing
*/
protected void publish(final Log buildLog) throws CruiseControlException {
setState(ProjectState.PUBLISHING);
for (final Publisher publisher : projectConfig.getPublishers()) {
// catch all errors, Publishers shouldn't cause failures in the build method
try {
publisher.publish(buildLog.getContent());
} catch (Throwable t) {
final StringBuilder message = new StringBuilder("exception publishing results");
message.append(" with ").append(publisher.getClass().getName());
message.append(" for project ").append(name);
LOG.error(message.toString(), t);
}
}
}
/**
* Iterate over all of the registered <code>Bootstrapper</code>s and call
* their respective <code>bootstrap</code> methods.
* @throws CruiseControlException if an error occurs during bootstrapping
*/
protected void bootstrap() throws CruiseControlException {
setState(ProjectState.BOOTSTRAPPING);
for (final Bootstrapper bootstrapper : projectConfig.getBootstrappers()) {
bootstrapper.bootstrap();
}
}
/**
* Ensure that label is valid for the specified LabelIncrementer
*
* @param oldLabel target label
* @param incrementer target LabelIncrementer
* @throws CruiseControlException if label is not valid
*/
protected void validateLabel(final String oldLabel, final LabelIncrementer incrementer)
throws CruiseControlException {
if (!incrementer.isValidLabel(oldLabel)) {
final String message = oldLabel + " is not a valid label for labelIncrementer "
+ incrementer.getClass().getName();
debug(message);
throw new CruiseControlException(message);
}
}
public boolean isLastBuildSuccessful() {
return wasLastBuildSuccessful;
}
void setWasLastBuildSuccessful(final boolean buildSuccessful) {
wasLastBuildSuccessful = buildSuccessful;
}
/**
* Logs a message to the application log, not to be confused with the
* CruiseControl build log.
* @param message the application message to log
*/
private void warn(final String message) {
LOG.warn("Project " + name + ": " + message);
}
private void info(final String message) {
LOG.info("Project " + name + ": " + message);
}
private void debug(final String message) {
LOG.debug("Project " + name + ": " + message);
}
public void start() {
if (stopped || getState() == ProjectState.STOPPED) {
stopped = false;
LOG.info("Project " + name + " starting");
setState(ProjectState.IDLE);
createNewSchedulingThread();
}
}
protected void createNewSchedulingThread() {
final Thread projectSchedulingThread = new Thread(this, "Project " + getName() + " thread");
projectSchedulingThread.start();
// brief nap to allow thread to start
try {
Thread.sleep(100);
} catch (InterruptedException ie) {
LOG.warn("interrupted while waiting for scheduling thread to start", ie);
}
}
public void stop() {
LOG.info("Project " + name + " stopping");
stopped = true;
setState(ProjectState.STOPPED);
synchronized (pausedMutex) {
pausedMutex.notifyAll();
}
synchronized (waitMutex) {
waitMutex.notifyAll();
}
synchronized (scheduleMutex) {
scheduleMutex.notifyAll();
}
}
public String toString() {
final StringBuilder sb = new StringBuilder("Project ");
sb.append(getName());
sb.append(": ");
sb.append(getStatus());
if (isPaused) {
sb.append(" (paused)");
}
return sb.toString();
}
public void addBuildProgressListener(final BuildProgressListener listener) {
synchronized (progressListeners) {
progressListeners.add(listener);
}
}
protected void fireProgressEvent(final BuildProgressEvent event) {
synchronized (progressListeners) {
for (final BuildProgressListener listener : progressListeners) {
listener.handleBuildProgress(event);
}
}
}
public void addBuildResultListener(final BuildResultListener listener) {
synchronized (resultListeners) {
resultListeners.add(listener);
}
}
protected void fireResultEvent(final BuildResultEvent event) {
synchronized (resultListeners) {
for (final BuildResultListener listener : resultListeners) {
listener.handleBuildResult(event);
}
}
}
List<Listener> getListeners() {
return projectConfig.getListeners();
}
public void setProjectConfig(final ProjectConfig projectConfig) throws CruiseControlException {
if (projectConfig == null) {
throw new IllegalArgumentException("project config can't be null");
}
this.projectConfig = projectConfig;
setLabelIncrementer(projectConfig.getLabelIncrementer());
}
void notifyListeners(final ProjectEvent event) {
if (projectConfig == null) {
throw new IllegalStateException("projectConfig is null");
}
for (final Listener listener : projectConfig.getListeners()) {
try {
listener.handleEvent(event);
} catch (CruiseControlException e) {
final StringBuilder message = new StringBuilder("exception notifying listener ");
message.append(listener.getClass().getName());
message.append(" for project ").append(name);
LOG.error(message.toString(), e);
}
}
}
public boolean equals(final Object arg0) {
if (arg0 == null) {
return false;
}
if (arg0.getClass().getName().equals(getClass().getName())) {
final Project thatProject = (Project) arg0;
return thatProject.name.equals(name);
}
return false;
}
public int hashCode() {
return name.hashCode();
}
public void register(final MBeanServer server) throws JMException {
LOG.debug("Registering project mbean");
final ProjectController projectController = new ProjectController(this);
projectController.register(server);
}
public ProjectConfig getProjectConfig() {
return projectConfig;
}
public Date getLastBuildDate() {
return lastBuild;
}
public Progress getProgress() {
return progress;
}
public List<String> getLogLabels() {
return projectConfig.getLogLabels();
}
public String[] getLogLabelLines(final String logLabel, final int firstLine) {
return projectConfig.getLogLabelLines(logLabel, firstLine);
}
@Override
public Map<String, String> getProperties() {
return projectConfig.getProperties();
}
@Override
public List<Modification> modificationsSinceLastBuild() {
final ModificationSet modificationSet = projectConfig.getModificationSet();
// modificationSet can be null when no modification set is set
if (modificationSet == null) {
warn("No modification set got from " + projectConfig.getName() + ", pretending 'not-modified status'");
return Collections.emptyList();
}
info("Getting changes since last successful build");
modificationSet.retrieveModificationsAsElement(lastSuccessfulBuild, progress);
return modificationSet.getCurrentModifications();
}
@Override
public List<Modification> modificationsSince(final Date since) {
final List<Modification> modifications = new ArrayList<Modification>();
final String logDirectory = getLog().getLogDir();
final List<String> logs = getLog().getLogLabels();
// The log directory was not set, print warning and return empty list
if (logDirectory == null) {
LOG.warn("Unable to get modificatiosn since " + since + " as the project[" + getName()
+ "] has no <log /> configured");
return modifications;
}
// Read all the build-successful log files since the given time
try {
for (final String logName : logs) {
// Skip those not successful and those too old
if (!Log.wasSuccessfulBuild(logName) || Log.parseDateFromLogFileName(logName).before(since)) {
continue;
}
// Read the XML
final Element logData = Util.loadRootElement(new File(logDirectory, logName));
final XMLLogHelper logElem = new XMLLogHelper(logData);
modifications.addAll(logElem.getModifications());
}
} catch (Exception e) {
LOG.error("Error checking for modifications", e);
}
return modifications;
}
@Override
public Date successLastBuild() {
return buildCounter == 0 ? new Date(0) : lastSuccessfulBuild;
}
@Override
public String successLastLabel() {
return buildCounter == 0 ? "" : label;
}
@Override
public String successLastLog() {
final List<String> labels = getLogLabels();
final String latest = successLastLabel();
for (String l : labels) {
if (latest.equals(Log.parseLabelFromLogFileName(l))) {
return l;
}
}
// Not build yet
return "";
}
}