/********************************************************************************
* CruiseControl, a Continuous Integration Toolkit
* Copyright (c) 2001, 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.FileInputStream;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import javax.management.JMException;
import javax.management.MBeanServer;
import net.sourceforge.cruisecontrol.config.DefaultPropertiesPlugin;
import net.sourceforge.cruisecontrol.config.PluginPlugin;
import net.sourceforge.cruisecontrol.config.PropertiesPlugin;
import net.sourceforge.cruisecontrol.gendoc.annotations.Cardinality;
import net.sourceforge.cruisecontrol.gendoc.annotations.Default;
import net.sourceforge.cruisecontrol.gendoc.annotations.Optional;
import net.sourceforge.cruisecontrol.gendoc.annotations.Required;
import net.sourceforge.cruisecontrol.gendoc.annotations.SkipDoc;
import net.sourceforge.cruisecontrol.gendoc.annotations.Description;
import net.sourceforge.cruisecontrol.labelincrementers.DefaultLabelIncrementer;
import net.sourceforge.cruisecontrol.util.ValidationHelper;
import org.apache.log4j.Logger;
/**
* @author <a href="mailto:jerome@coffeebreaks.org">Jerome Lacoste</a>
*/
@Description("<p>A <code><project></code> is the basic unit of work - it will "
+ "handle checking for modifications, building, and publishing the results of "
+ "your project.</p>"
+ "<p>Note: one config.xml file can contain several <code><project></code> "
+ "elements; these projects will all run in a shared build queue (see the "
+ "<a href='#threads'><code><threads></code></a> element if you want to build "
+ "multiple projects at the same time).</p>")
public class ProjectConfig implements ProjectInterface {
private static final long serialVersionUID = -893779421250033198L;
private static final Logger LOG = Logger.getLogger(ProjectConfig.class);
private String name;
private boolean buildAfterFailed = true;
private boolean forceOnly = false;
private boolean requiremodification = true;
private boolean forceBuildNewProject = true; // default to current behavior
private transient Bootstrappers bootstrappers;
private transient LabelIncrementer labelIncrementer;
private transient Listeners listeners;
private transient Log log;
private transient ModificationSet modificationSet;
private transient Publishers publishers;
private transient Schedule schedule;
private Project project;
/**
* Called after the configuration is read to make sure that all the mandatory parameters were specified..
*
* @throws CruiseControlException
* if there was a configuration error.
*/
public void validate() throws CruiseControlException {
ValidationHelper.assertTrue(schedule != null, "project requires a schedule");
if (labelIncrementer == null) {
labelIncrementer = new DefaultLabelIncrementer();
}
if (bootstrappers != null) {
bootstrappers.validate();
}
if (listeners != null) {
listeners.validate();
}
if (log == null) {
log = new Log();
}
log.setProjectName(name);
log.validate();
if (modificationSet != null) {
modificationSet.validate();
}
if (schedule != null) {
schedule.validate();
}
if (publishers != null) {
publishers.validate();
}
}
@Description("Unique identifier for this project.")
@Required
public void setName(String name) {
this.name = name;
}
@Description("Should CruiseControl keep on building even though it has failed and no new "
+ "modifications are detected? This feature is useful if you want CruiseControl to "
+ "detect situations where a build fails because of outside dependencies (like "
+ "temporary failing database connection).")
@Default("true")
@Optional
public void setBuildAfterFailed(boolean buildAfterFailed) {
this.buildAfterFailed = buildAfterFailed;
}
@Description("Should CruiseControl force a project to build if the serial file (project.SER)"
+ " is not found, typically this is when the project is first added. This feature is "
+ "useful for projects that have or use dependencies.")
@Default("true")
@Optional
public void setForceBuildNewProject(boolean forceBuildNewProject) {
this.forceBuildNewProject = forceBuildNewProject;
}
@Description("Registers a general plug-in inherrited from PropertiesPlugin interface.")
public void add(PropertiesPlugin plugin) {
// Must be empty, plugin is registered somewhere else
}
/**
* @deprecated exists only for gendoc, should not be called.
*/
@SuppressWarnings("unused")
@Description("Defines a name/value pair used in configuration.")
public void add(DefaultPropertiesPlugin plugin) {
// FIXME currently only declared for documentation generation purposes
throw new IllegalStateException("GenDoc-only method should not be invoked.");
}
/**
* @param plugin plugin
* @deprecated exists only for gendoc, should not be called.
*/
@SuppressWarnings("unused")
@Description("Registers a classname with an alias.")
public void add(PluginPlugin plugin) {
// currently only declared for documentation generation purposes
throw new IllegalStateException("GenDoc-only method should not be invoked.");
}
@Description("Container element for Label Incrementer plugin instances.")
@Cardinality(min = 0, max = 1)
public void add(LabelIncrementer labelIncrementer) {
if (this.labelIncrementer != null) {
LOG.warn("replacing existing label incrememnter [" + this.labelIncrementer.toString()
+ "] with new one [" + labelIncrementer.toString() + "]");
}
this.labelIncrementer = labelIncrementer;
}
@Description("Container element for Listener plugin instances.")
@Cardinality(min = 0, max = 1)
public void add(Listeners listeners) {
this.listeners = listeners;
}
@Description("Container element for Source Control plugin instances.")
@Cardinality(min = 1, max = 1)
public void add(ModificationSet modificationSet) {
this.modificationSet = modificationSet;
}
@Description("Container element for Bootstrapper plugin instances.")
@Cardinality(min = 0, max = 1)
public void add(Bootstrappers bootstrappers) {
this.bootstrappers = bootstrappers;
}
@Description("Container element for Publisher plugin instances.")
@Cardinality(min = 0, max = 1)
public void add(Publishers publishers) {
this.publishers = publishers;
}
@Description("Specifies the SourceControl poll interval, and is a parent "
+ "element for Builder plugin instances.")
@Cardinality(min = 1, max = 1)
public void add(Schedule schedule) {
this.schedule = schedule;
}
@Description("Specifies where project log files are stored.")
@Cardinality(min = 0, max = 1)
public void add(Log log) {
this.log = log;
}
public boolean shouldBuildAfterFailed() {
return buildAfterFailed;
}
public Log getLog() {
return log;
}
public List<Bootstrapper> getBootstrappers() {
return bootstrappers == null ? Collections.<Bootstrapper>emptyList() : bootstrappers.getBootstrappers();
}
public List<Listener> getListeners() {
return listeners == null ? Collections.<Listener>emptyList() : listeners.getListeners();
}
public List<Publisher> getPublishers() {
return publishers == null ? Collections.<Publisher>emptyList() : publishers.getPublishers();
}
public ModificationSet getModificationSet() {
return modificationSet;
}
public Schedule getSchedule() {
return schedule;
}
public LabelIncrementer getLabelIncrementer() {
return labelIncrementer;
}
public String getName() {
return name;
}
@Description(
"<p>The <code><bootstrappers></code> element is a container element"
+ "for Bootstrapper plugin instances.</p>"
+ "<p>Bootstrappers are run before a build takes place, regardless of"
+ "whether a build is necessary or not, but not if the build is paused."
+ "Each bootstrapper element is independent of the others so it is quite"
+ "possible to have multiple bootstrappers of the same time, say 3 CVS or"
+ "VssBootstrappers to update 3 different files.</p>")
public static class Bootstrappers implements Serializable {
private static final long serialVersionUID = 7428779281399848035L;
private final List<Bootstrapper> bootstrappers = new ArrayList<Bootstrapper>();
@Description("Adds a bootstrapper to the project.")
@Cardinality(min = 0, max = -1)
public void add(final Bootstrapper bootstrapper) {
bootstrappers.add(bootstrapper);
}
public List<Bootstrapper> getBootstrappers() {
return bootstrappers;
}
public void validate() throws CruiseControlException {
for (final Bootstrapper nextBootstrapper : bootstrappers) {
nextBootstrapper.validate();
}
}
}
public static class Listeners implements Serializable {
private static final long serialVersionUID = -3816080104514876038L;
private final List<Listener> listeners = new ArrayList<Listener>();
public void add(Listener listener) {
listeners.add(listener);
}
public List<Listener> getListeners() {
return listeners;
}
public void validate() throws CruiseControlException {
for (final Listener nextListener : listeners) {
nextListener.validate();
}
}
}
public static class Publishers implements Serializable {
private static final long serialVersionUID = -410933401108345152L;
private final List<Publisher> publishers = new ArrayList<Publisher>();
public void add(Publisher publisher) {
publishers.add(publisher);
}
public List<Publisher> getPublishers() {
return publishers;
}
public void validate() throws CruiseControlException {
for (final Publisher nextPublisher : publishers) {
nextPublisher.validate();
}
}
}
@Description("Indicate that the build for the project only occurs when forced. Note "
+ "that if the buildAfterFailed attribute is true, then builds will continue to "
+ "occur based upon the the rules on <a href='#schedule'><code><schedule></code></a> "
+ "until the build is successful.")
@Optional
@Default("false")
public void setForceOnly(boolean forceOnly) {
this.forceOnly = forceOnly;
}
/**
* @return the forceOnly
*/
public boolean isForceOnly() {
return forceOnly;
}
/**
* @return the requiremodification
*/
public boolean isRequiremodification() {
return requiremodification;
}
/**
* @param requiremodification
* the requiremodification to set
*/
@Description("Is a modification required for the build to continue? Default value is "
+ "true. Useful to set to false when the schedule has only time based builds or if "
+ "you want to run tests to verify an external resource (such as a database).")
@Default("true")
@Optional
public void setRequiremodification(boolean requiremodification) {
this.requiremodification = requiremodification;
}
public void configureProject() throws CruiseControlException {
Project myProject = readProject(name);
myProject.setName(name);
myProject.setProjectConfig(this);
myProject.init();
this.project = myProject;
}
/**
* Reads project configuration from a previously serialized Project or creates a new instance. The name of the
* serialized project file is derived from the name of the project.
*
* @param projectName
* name of the serialized project
* @return Deserialized Project or a new Project if there are any problems reading the serialized Project; should
* never return null
*/
Project readProject(final String projectName) {
File serializedProjectFile = new File(Builder.getFileSystemSafeProjectName(projectName) + ".ser");
LOG.debug("Reading serialized project from: " + serializedProjectFile.getAbsolutePath());
if (!serializedProjectFile.exists() || !serializedProjectFile.canRead()) {
serializedProjectFile = ProjectConfig.tryOldSerializedFileName(projectName);
}
if (!serializedProjectFile.exists() || !serializedProjectFile.canRead()
|| serializedProjectFile.isDirectory()) {
final Project newProject = new Project();
newProject.setName(projectName);
if (forceBuildNewProject) {
LOG.warn("No previously serialized project found [" + serializedProjectFile.getAbsolutePath()
+ ".ser], forcing a build.");
newProject.setBuildForced(true);
} else {
LOG.warn("No previously serialized project found [" + serializedProjectFile.getAbsolutePath()
+ ".ser], Not forcing build, forceBuildNewProject = false.");
}
return newProject;
}
try {
final ObjectInputStream s = new ObjectInputStream(new FileInputStream(serializedProjectFile));
try {
return (Project) s.readObject();
} finally {
s.close();
}
} catch (Exception e) {
LOG.warn("Error deserializing project file from " + serializedProjectFile.getAbsolutePath(), e);
return new Project();
}
}
private static File tryOldSerializedFileName(String projectName) {
File serializedProjectFile;
serializedProjectFile = new File(projectName);
LOG.debug(projectName + ".ser not found, looking for serialized project file "
+ serializedProjectFile.getAbsolutePath());
return serializedProjectFile;
}
public boolean equals(Object arg0) {
if (arg0 == null) {
return false;
}
if (arg0.getClass().getName().equals(getClass().getName())) {
ProjectConfig thatProject = (ProjectConfig) arg0;
return thatProject.name.equals(name);
}
return false;
}
public int hashCode() {
return name.hashCode();
}
/**
* Need to delegate to "project" toString() to avoid breaking external jmx scripts.
* {@inheritDoc}
*/
public String toString() {
if (project != null) {
return project.toString();
}
return super.toString();
}
public void getStateFromOldProject(ProjectInterface oldProject) throws CruiseControlException {
ProjectConfig oldProjectConfig = (ProjectConfig) oldProject;
project = oldProjectConfig.project;
project.setProjectConfig(this);
project.init();
}
public void execute() {
project.execute();
}
public void register(MBeanServer server) throws JMException {
project.register(server);
}
@SkipDoc
public void setBuildQueue(BuildQueue buildQueue) {
project.setBuildQueue(buildQueue);
}
public void start() {
project.start();
}
public void stop() {
project.stop();
}
// TODO remove this. only here till tests are fixed up.
Project getProject() {
return project;
}
public String getStatus() {
return project.getStatus();
}
public String getBuildStartTime() {
return project.getBuildStartTime();
}
public boolean isPaused() {
return project.isPaused();
}
public List<Modification> getModifications() {
if (getModificationSet() != null) {
return getModificationSet().getCurrentModifications();
} else {
return Collections.emptyList();
}
}
public boolean isInState(ProjectState state) {
return project.getState().equals(state);
}
public List<String> getLogLabels() {
return log.getLogLabels();
}
public String[] getLogLabelLines(final String logLabel, final int firstLine) {
return log.getLogLabelLines(logLabel, firstLine);
}
public Map<String, String> getProperties() {
return project.getProperties();
}
public List<Modification> modificationsSinceLastBuild() {
return project.modificationsSinceLastBuild();
}
public List<Modification> modificationsSince(Date since) {
return project.modificationsSince(since);
}
public Date successLastBuild() {
return project.successLastBuild();
}
public String getLogDir() {
return log.getLogDir();
}
public String successLastLabel() {
return project.successLastLabel();
}
public String successLastLog() {
return project.successLastLog();
}
}