/* * The MIT License * * Copyright (c) 2004-2011, Oracle Corporation, Kohsuke Kawaguchi, * Brian Westrich, Erik Ramfelt, Ertan Deniz, Jean-Baptiste Quenot, * Luca Domenico Milanesio, R. Tyler Ballance, Stephen Connolly, Tom Huybrechts, * id:cactusman, Yahoo! Inc., Anton Kozak, Nikita Levyankov * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package hudson.model; import antlr.ANTLRException; import hudson.AbortException; import hudson.CopyOnWrite; import hudson.FeedAdapter; import hudson.FilePath; import hudson.Functions; import hudson.Launcher; import hudson.Util; import hudson.cli.declarative.CLIMethod; import hudson.cli.declarative.CLIResolver; import hudson.diagnosis.OldDataMonitor; import hudson.model.Cause.LegacyCodeCause; import hudson.model.Cause.RemoteCause; import hudson.model.Cause.UserCause; import hudson.model.Descriptor.FormException; import hudson.model.Fingerprint.RangeSet; import hudson.model.Queue.Executable; import hudson.model.Queue.Task; import hudson.model.Queue.WaitingItem; import hudson.model.RunMap.Constructor; import hudson.model.queue.CauseOfBlockage; import hudson.model.queue.SubTask; import hudson.model.queue.SubTaskContributor; import hudson.scm.ChangeLogSet; import hudson.scm.ChangeLogSet.Entry; import hudson.scm.NullSCM; import hudson.scm.PollingResult; import hudson.scm.SCM; import hudson.scm.SCMRevisionState; import hudson.scm.SCMS; import hudson.search.SearchIndexBuilder; import hudson.security.Permission; import hudson.slaves.WorkspaceList; import hudson.tasks.BuildStep; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.BuildTrigger; import hudson.tasks.BuildWrapperDescriptor; import hudson.tasks.Mailer; import hudson.tasks.Publisher; import hudson.triggers.SCMTrigger; import hudson.triggers.Trigger; import hudson.triggers.TriggerDescriptor; import hudson.util.AutoCompleteSeeder; import hudson.util.CascadingUtil; import hudson.util.DescribableList; import hudson.util.DescribableListUtil; import hudson.util.EditDistance; import hudson.util.FormValidation; import hudson.widgets.BuildHistoryWidget; import hudson.widgets.HistoryWidget; import java.io.File; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.GregorianCalendar; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import java.util.Vector; import java.util.concurrent.Future; import java.util.logging.Level; import java.util.logging.Logger; import javax.servlet.ServletException; import javax.servlet.http.HttpServletResponse; import net.sf.json.JSONObject; import org.apache.commons.lang3.math.NumberUtils; import org.hudsonci.api.model.IAbstractProject; import org.hudsonci.model.project.property.IntegerProjectProperty; import org.hudsonci.model.project.property.TriggerProjectProperty; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.CmdLineException; import org.kohsuke.stapler.ForwardToView; import org.kohsuke.stapler.HttpRedirect; import org.kohsuke.stapler.HttpResponse; import org.kohsuke.stapler.HttpResponses; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.export.Exported; import static hudson.scm.PollingResult.*; import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; /** * Base implementation of {@link Job}s that build software. * * For now this is primarily the common part of {@link Project} and MavenModule. * * @author Kohsuke Kawaguchi * @see AbstractBuild */ public abstract class AbstractProject<P extends AbstractProject<P, R>, R extends AbstractBuild<P, R>> extends Job<P, R> implements BuildableItem, IAbstractProject { public static final String CONCURRENT_BUILD_PROPERTY_NAME = "concurrentBuild"; public static final String CLEAN_WORKSPACE_REQUIRED_PROPERTY_NAME = "cleanWorkspaceRequired"; public static final String BLOCK_BUILD_WHEN_DOWNSTREAM_BUILDING_PROPERTY_NAME = "blockBuildWhenDownstreamBuilding"; public static final String BLOCK_BUILD_WHEN_UPSTREAM_BUILDING_PROPERTY_NAME = "blockBuildWhenUpstreamBuilding"; public static final String QUIET_PERIOD_PROPERTY_NAME = "quietPeriod"; public static final String SCM_CHECKOUT_RETRY_COUNT_PROPERTY_NAME = "scmCheckoutRetryCount"; public static final String CUSTOM_WORKSPACE_PROPERTY_NAME = "customWorkspace"; public static final String JDK_PROPERTY_NAME = "jdk"; public static final String SCM_PROPERTY_NAME = "scm"; public static final String HAS_QUIET_PERIOD_PROPERTY_NAME = "hasQuietPeriod"; public static final String HAS_SCM_CHECKOUT_RETRY_COUNT_PROPERTY_NAME = "hasScmCheckoutRetryCount"; public static final String BUILD_TRIGGER_PROPERTY_NAME = "hudson-tasks-BuildTrigger"; public static final String APPOINTED_NODE_PROPERTY_NAME = "appointedNode"; public static final String BASIC_KEY = "basic"; public static final String AFFINITY_CHO0SER_KEY = "affinityChooser"; public static final String SLAVE_KEY = "slave"; public static final String ASSIGNED_LABEL_KEY = "_.assignedLabelString"; /** * {@link SCM} associated with the project. * To allow derived classes to link {@link SCM} config to elsewhere, * access to this variable should always go through {@link #getScm()}. * @deprecated as of 2.2.0 * don't use this field directly, logic was moved to {@link org.hudsonci.api.model.IProjectProperty}. * Use getter/setter for accessing to this field. */ @Deprecated private volatile SCM scm = new NullSCM(); /** * State returned from {@link SCM#poll(AbstractProject, Launcher, FilePath, TaskListener, SCMRevisionState)}. */ private volatile transient SCMRevisionState pollingBaseline = null; /** * All the builds keyed by their build number. */ protected transient /*almost final*/ RunMap<R> builds = new RunMap<R>(); /** * The quiet period. Null to delegate to the system default. * @deprecated as of 2.2.0 * don't use this field directly, logic was moved to {@link org.hudsonci.api.model.IProjectProperty}. * Use getter/setter for accessing to this field. */ @Deprecated private volatile Integer quietPeriod = null; /** * The retry count. Null to delegate to the system default. * @deprecated as of 2.2.0 * don't use this field directly, logic was moved to {@link org.hudsonci.api.model.IProjectProperty}. * Use getter/setter for accessing to this field. */ @Deprecated private volatile Integer scmCheckoutRetryCount = null; /** * If this project is configured to be only built on a certain label, * this value will be set to that label. * * For historical reasons, this is called 'assignedNode'. Also for * a historical reason, null to indicate the affinity * with the master node. * * @see #canRoam * * @deprecated as of 2.2.0 * don't use this field directly, logic was moved to * {@link hudson.model.AbstractProject#getAppointedNode()#getName()}. */ @Deprecated private String assignedNode; /** * Node list is dropdown or textfield * * @deprecated as of 2.2.0 * don't use this field directly, logic was moved to * {@link hudson.model.AbstractProject#getAppointedNode()#isAdvancedAffinityChooser()}. */ @Deprecated private Boolean advancedAffinityChooser; /** * True if this project can be built on any node. * * <p> * This somewhat ugly flag combination is so that we can migrate * existing Hudson installations nicely. * * @deprecated as of 2.2.0 * don't use this field directly, logic was moved to * {@link hudson.model.AbstractProject#getAppointedNode()#getCanRoam}. */ @Deprecated private volatile boolean canRoam; /** * True to suspend new builds. */ protected volatile boolean disabled; /** * True to keep builds of this project in queue when downstream projects are building. * * @deprecated as of 2.1.2. * don't use this field directly, logic was moved to {@link org.hudsonci.api.model.IProjectProperty}. * Use getter/setter for accessing to this field. */ @Deprecated protected volatile boolean blockBuildWhenDownstreamBuilding; /** * True to keep builds of this project in queue when upstream projects are building. * * @deprecated as of 2.2.0 * don't use this field directly, logic was moved to {@link org.hudsonci.api.model.IProjectProperty}. * Use getter/setter for accessing to this field. */ @Deprecated protected volatile boolean blockBuildWhenUpstreamBuilding; /** * Identifies {@link JDK} to be used. * Null if no explicit configuration is required. * <p/> * <p/> * Can't store {@link JDK} directly because {@link Hudson} and {@link Project} * are saved independently. * * @see Hudson#getJDK(String) * @deprecated 2.2.0 * don't use this field directly, logic was moved to {@link org.hudsonci.api.model.IProjectProperty}. * Use getter/setter for accessing to this field. */ @Deprecated private volatile String jdk; /** * @deprecated since 2007-01-29. */ @Deprecated private transient boolean enableRemoteTrigger; private volatile BuildAuthorizationToken authToken = null; /** * List of all {@link Trigger}s for this project. * * @deprecated as of 2.2.0 * * don't use this field directly, logic was moved to {@link org.hudsonci.api.model.IProjectProperty}. * Use getter/setter for accessing to this field. */ protected List<Trigger<?>> triggers = new Vector<Trigger<?>>(); /** * {@link Action}s contributed from subsidiary objects associated with * {@link AbstractProject}, such as from triggers, builders, publishers, etc. * * We don't want to persist them separately, and these actions * come and go as configuration change, so it's kept separate. */ @CopyOnWrite protected transient volatile List<Action> transientActions = new Vector<Action>(); /** * @deprecated as of 2.2.0 * don't use this field directly, logic was moved to {@link org.hudsonci.api.model.IProjectProperty}. * Use getter/setter for accessing to this field. */ @Deprecated private boolean concurrentBuild; /** * True to clean the workspace prior to each build. * * @deprecated as of 2.2.0 * don't use this field directly, logic was moved to {@link org.hudsonci.api.model.IProjectProperty}. * Use getter/setter for accessing to this field. */ @Deprecated private volatile boolean cleanWorkspaceRequired; protected AbstractProject(ItemGroup parent, String name) { super(parent, name); //TODO: Investigate when this case happens. //if (Hudson.getInstance() != null && !Hudson.getInstance().getNodes().isEmpty()) { // if a new job is configured with Hudson that already has slave nodes // make it roamable by default // canRoam = true; //} } @Override public void onCreatedFromScratch() { super.onCreatedFromScratch(); // solicit initial contributions, especially from TransientProjectActionFactory updateTransientActions(); setCreationTime(new GregorianCalendar().getTimeInMillis()); User user = User.current(); if (user != null){ setCreatedBy(user.getId()); grantProjectMatrixPermissions(user); } } @Override public void onLoad(ItemGroup<? extends Item> parent, String name) throws IOException { super.onLoad(parent, name); this.builds = new RunMap<R>(); this.builds.load(this, new Constructor<R>() { public R create(File dir) throws IOException { return loadBuild(dir); } }); // boolean! Can't tell if xml file contained false.. if (enableRemoteTrigger) OldDataMonitor.report(this, "1.77"); for (Trigger t : getTriggerDescribableList()) { t.start(this,false); } if(scm==null) scm = new NullSCM(); // perhaps it was pointing to a plugin that no longer exists. if(transientActions==null) transientActions = new Vector<Action>(); // happens when loaded from disk updateTransientActions(); getTriggerDescribableList().setOwner(this); } @Override protected void buildProjectProperties() throws IOException { super.buildProjectProperties(); convertBlockBuildWhenUpstreamBuildingProperty(); convertBlockBuildWhenDownstreamBuildingProperty(); convertConcurrentBuildProperty(); convertCleanWorkspaceRequiredProperty(); convertQuietPeriodProperty(); convertScmCheckoutRetryCountProperty(); convertJDKProperty(); convertScmProperty(); convertTriggerProperties(); convertAppointedNode(); } void convertBlockBuildWhenUpstreamBuildingProperty() throws IOException { if (null == getProperty(BLOCK_BUILD_WHEN_UPSTREAM_BUILDING_PROPERTY_NAME)) { setBlockBuildWhenUpstreamBuilding(blockBuildWhenUpstreamBuilding); blockBuildWhenUpstreamBuilding = false; } } void convertBlockBuildWhenDownstreamBuildingProperty() throws IOException { if (null == getProperty(BLOCK_BUILD_WHEN_DOWNSTREAM_BUILDING_PROPERTY_NAME)) { setBlockBuildWhenDownstreamBuilding(blockBuildWhenDownstreamBuilding); blockBuildWhenDownstreamBuilding = false; } } void convertConcurrentBuildProperty() throws IOException { if (null == getProperty(CONCURRENT_BUILD_PROPERTY_NAME)) { setConcurrentBuild(concurrentBuild); concurrentBuild = false; } } void convertCleanWorkspaceRequiredProperty() throws IOException { if (null == getProperty(CLEAN_WORKSPACE_REQUIRED_PROPERTY_NAME)) { setCleanWorkspaceRequired(cleanWorkspaceRequired); cleanWorkspaceRequired = false; } } void convertQuietPeriodProperty() throws IOException { if (null != quietPeriod && null == getProperty(QUIET_PERIOD_PROPERTY_NAME)) { setQuietPeriod(quietPeriod); quietPeriod = null; } } void convertScmCheckoutRetryCountProperty() throws IOException { if (null != scmCheckoutRetryCount && null == getProperty(SCM_CHECKOUT_RETRY_COUNT_PROPERTY_NAME)) { setScmCheckoutRetryCount(scmCheckoutRetryCount); scmCheckoutRetryCount = null; } } void convertJDKProperty() throws IOException { if (null != jdk && null == getProperty(JDK_PROPERTY_NAME)) { setJDK(jdk); jdk = null; } } void convertScmProperty() throws IOException { if (null != scm && null == getProperty(SCM_PROPERTY_NAME)) { setScm(scm); scm = null; } } void convertTriggerProperties() { if (triggers != null) { setTriggers(triggers); triggers = null; } } void convertAppointedNode() { if (assignedNode != null && getProperty(APPOINTED_NODE_PROPERTY_NAME) == null) { setAppointedNode(new AppointedNode(assignedNode, advancedAffinityChooser)); assignedNode = null; advancedAffinityChooser = null; } } @Override protected void performDelete() throws IOException, InterruptedException { // prevent a new build while a delete operation is in progress makeDisabled(true); FilePath ws = getWorkspace(); if(ws!=null) { Node on = getLastBuiltOn(); getScm().processWorkspaceBeforeDeletion(this, ws, on); if(on!=null) on.getFileSystemProvisioner().discardWorkspace(this,ws); } super.performDelete(); } /** * Does this project perform concurrent builds? * @since 1.319 */ @Exported public boolean isConcurrentBuild() { return Hudson.CONCURRENT_BUILD && CascadingUtil.getBooleanProjectProperty(this, CONCURRENT_BUILD_PROPERTY_NAME).getValue(); } public void setConcurrentBuild(boolean b) throws IOException { CascadingUtil.getBooleanProjectProperty(this, CONCURRENT_BUILD_PROPERTY_NAME).setValue(b); save(); } public boolean isCleanWorkspaceRequired() { return CascadingUtil.getBooleanProjectProperty(this, CLEAN_WORKSPACE_REQUIRED_PROPERTY_NAME).getValue(); } public void setCleanWorkspaceRequired(boolean cleanWorkspaceRequired) { CascadingUtil.getBooleanProjectProperty(this, CLEAN_WORKSPACE_REQUIRED_PROPERTY_NAME).setValue(cleanWorkspaceRequired); } /** * If this project is configured to be always built on this node, * return that {@link Node}. Otherwise null. */ public Label getAssignedLabel() { return getAppointedNode() == null ? null : getAppointedNode().getAssignedLabel(); } /** * Gets the textual representation of the assigned label as it was entered by the user. */ public String getAssignedLabelString() { return getAppointedNode() == null ? null : getAppointedNode().getAssignedLabelString(); } /** * Sets the assigned label. * @param label node label. * * @throws java.io.IOException exception. */ public void setAssignedLabel(Label label) throws IOException { AppointedNode node = getAppointedNode(); if(node == null){ node = new AppointedNode(); setAppointedNode(node); } node.setAssignedLabel(label); save(); } /** * Assigns this job to the given node. A convenience method over {@link #setAssignedLabel(Label)}. * * @param node node. * @throws java.io.IOException exception */ public void setAssignedNode(Node node) throws IOException { setAssignedLabel(node.getSelfLabel()); } /** * Gets whether this project is using the advanced affinity chooser UI. * * @return true - advanced chooser, false - simple textfield. */ public boolean isAdvancedAffinityChooser() { //For newly created project advanced chooser is not used. //Set value to false in order to avoid NullPointerException return getAppointedNode() != null && getAppointedNode().getAdvancedAffinityChooser(); } /** * Sets whether this project is using the advanced affinity chooser UI. * * @param b true - advanced chooser, false - otherwise * @throws java.io.IOException exception. */ public void setAdvancedAffinityChooser(boolean b) throws IOException { AppointedNode node = getAppointedNode(); if(node == null){ node = new AppointedNode(); setAppointedNode(node); } node.setAdvancedAffinityChooser(b); save(); } /** * Sets {@link AppointedNode}. * * @param appointedNode {@link AppointedNode}. */ @SuppressWarnings("unchecked") public void setAppointedNode(AppointedNode appointedNode) { CascadingUtil.getBaseProjectProperty(this, APPOINTED_NODE_PROPERTY_NAME).setValue(appointedNode); } /** * Returns {@link AppointedNode}. Returned value is not null. * * @return appointedNode {@link AppointedNode}. */ public AppointedNode getAppointedNode() { return (AppointedNode)CascadingUtil.getBaseProjectProperty(this, APPOINTED_NODE_PROPERTY_NAME).getValue(); } /** * Get the term used in the UI to represent this kind of {@link AbstractProject}. * Must start with a capital letter. */ @Override public String getPronoun() { return Messages.AbstractProject_Pronoun(); } /** * Returns the root project value. * * @return the root project value. */ public AbstractProject getRootProject() { if (this.getParent() instanceof Hudson) { return this; } else { return ((AbstractProject) this.getParent()).getRootProject(); } } /** * Gets the directory where the module is checked out. * * @return * null if the workspace is on a slave that's not connected. * @deprecated as of 1.319 * To support concurrent builds of the same project, this method is moved to {@link AbstractBuild}. * For backward compatibility, this method returns the right {@link AbstractBuild#getWorkspace()} if called * from {@link Executor}, and otherwise the workspace of the last build. * * <p> * If you are calling this method during a build from an executor, switch it to {@link AbstractBuild#getWorkspace()}. * If you are calling this method to serve a file from the workspace, doing a form validation, etc., then * use {@link #getSomeWorkspace()} */ public final FilePath getWorkspace() { AbstractBuild b = getBuildForDeprecatedMethods(); return b != null ? b.getWorkspace() : null; } /** * Various deprecated methods in this class all need the 'current' build. This method returns * the build suitable for that purpose. * * @return An AbstractBuild for deprecated methods to use. */ private AbstractBuild getBuildForDeprecatedMethods() { Executor e = Executor.currentExecutor(); if(e!=null) { Executable exe = e.getCurrentExecutable(); if (exe instanceof AbstractBuild) { AbstractBuild b = (AbstractBuild) exe; if(b.getProject()==this) return b; } } R lb = getLastBuild(); if(lb!=null) return lb; return null; } /** * Gets a workspace for some build of this project. * * <p> * This is useful for obtaining a workspace for the purpose of form field validation, where exactly * which build the workspace belonged is less important. The implementation makes a cursory effort * to find some workspace. * * @return * null if there's no available workspace. * @since 1.319 */ public final FilePath getSomeWorkspace() { R b = getSomeBuildWithWorkspace(); return b!=null ? b.getWorkspace() : null; } /** * Gets some build that has a live workspace. * * @return null if no such build exists. */ public final R getSomeBuildWithWorkspace() { int cnt=0; for (R b = getLastBuild(); cnt<5 && b!=null; b=b.getPreviousBuild()) { FilePath ws = b.getWorkspace(); if (ws!=null) return b; } return null; } /** * Returns the root directory of the checked-out module. * <p> * This is usually where <tt>pom.xml</tt>, <tt>build.xml</tt> * and so on exists. * * @deprecated as of 1.319 * See {@link #getWorkspace()} for a migration strategy. */ public FilePath getModuleRoot() { AbstractBuild b = getBuildForDeprecatedMethods(); return b != null ? b.getModuleRoot() : null; } /** * Returns the root directories of all checked-out modules. * <p> * Some SCMs support checking out multiple modules into the same workspace. * In these cases, the returned array will have a length greater than one. * @return The roots of all modules checked out from the SCM. * * @deprecated as of 1.319 * See {@link #getWorkspace()} for a migration strategy. */ public FilePath[] getModuleRoots() { AbstractBuild b = getBuildForDeprecatedMethods(); return b != null ? b.getModuleRoots() : null; } public int getQuietPeriod() { IntegerProjectProperty property = CascadingUtil.getIntegerProjectProperty(this, QUIET_PERIOD_PROPERTY_NAME); Integer value = property.getValue(); return property.getDefaultValue().equals(value) ? Hudson.getInstance().getQuietPeriod() : value; } /** * Sets the custom quiet period of this project, or revert to the global default if null is given. * * @param seconds quiet period * @throws IOException if any. */ public void setQuietPeriod(Integer seconds) throws IOException { CascadingUtil.getIntegerProjectProperty(this, QUIET_PERIOD_PROPERTY_NAME).setValue(seconds); save(); } public int getScmCheckoutRetryCount() { IntegerProjectProperty property = CascadingUtil.getIntegerProjectProperty(this, SCM_CHECKOUT_RETRY_COUNT_PROPERTY_NAME); Integer value = property.getValue(); return property.getDefaultValue().equals(value) ? Hudson.getInstance().getScmCheckoutRetryCount() : value; } public void setScmCheckoutRetryCount(Integer retryCount) { CascadingUtil.getIntegerProjectProperty(this, SCM_CHECKOUT_RETRY_COUNT_PROPERTY_NAME).setValue(retryCount); } /** * Sets scmCheckoutRetryCount, Uses {@link NumberUtils#isNumber(String)} for checking retryCount param. * If it is not valid number, null will be set. * * @param scmCheckoutRetryCount retry count. * @throws IOException if any. */ protected void setScmCheckoutRetryCount(String scmCheckoutRetryCount) throws IOException { Integer retryCount = null; if (NumberUtils.isNumber(scmCheckoutRetryCount)) { retryCount = NumberUtils.createInteger(scmCheckoutRetryCount); } setScmCheckoutRetryCount(retryCount); } /** * @return true if quiet period was configured. * @deprecated as of 2.1.2 * This method was used only on UI side. No longer required. */ // ugly name because of EL public boolean getHasCustomQuietPeriod() { return null != CascadingUtil.getIntegerProjectProperty(this, QUIET_PERIOD_PROPERTY_NAME).getValue(); } /** * Sets quietPeriod, Uses {@link NumberUtils#isNumber(String)} for checking seconds param. If seconds is not valid * number, null will be set. * * @param seconds quiet period. * @throws IOException if any. */ protected void setQuietPeriod(String seconds) throws IOException { Integer period = null; if (NumberUtils.isNumber(seconds)) { period = NumberUtils.createInteger(seconds); } setQuietPeriod(period); } /** * Checks whether scmRetryCount is configured * * @return true if yes, false - otherwise. * @deprecated as of 2.1.2 */ public boolean hasCustomScmCheckoutRetryCount(){ return null != CascadingUtil.getIntegerProjectProperty(this, SCM_CHECKOUT_RETRY_COUNT_PROPERTY_NAME).getValue(); } @Override public boolean isBuildable() { return !isDisabled() && !isHoldOffBuildUntilSave(); } /** * Used in <tt>sidepanel.jelly</tt> to decide whether to display * the config/delete/build links. */ public boolean isConfigurable() { return true; } public boolean blockBuildWhenDownstreamBuilding() { return CascadingUtil.getBooleanProjectProperty(this, BLOCK_BUILD_WHEN_DOWNSTREAM_BUILDING_PROPERTY_NAME).getValue(); } public void setBlockBuildWhenDownstreamBuilding(boolean b) throws IOException { CascadingUtil.getBooleanProjectProperty(this, BLOCK_BUILD_WHEN_DOWNSTREAM_BUILDING_PROPERTY_NAME).setValue(b); save(); } public boolean blockBuildWhenUpstreamBuilding() { return CascadingUtil.getBooleanProjectProperty(this, BLOCK_BUILD_WHEN_UPSTREAM_BUILDING_PROPERTY_NAME).getValue(); } public void setBlockBuildWhenUpstreamBuilding(boolean b) throws IOException { CascadingUtil.getBooleanProjectProperty(this, BLOCK_BUILD_WHEN_UPSTREAM_BUILDING_PROPERTY_NAME).setValue(b); save(); } public boolean isDisabled() { return disabled; } /** * Validates the retry count Regex */ public FormValidation doCheckRetryCount(@QueryParameter String value)throws IOException,ServletException{ // retry count is optional so this is ok if(value == null || value.trim().equals("")) return FormValidation.ok(); if (!value.matches("[0-9]*")) { return FormValidation.error("Invalid retry count"); } return FormValidation.ok(); } /** * Marks the build as disabled. */ public void makeDisabled(boolean b) throws IOException { if(disabled==b) return; // noop this.disabled = b; if(b) Hudson.getInstance().getQueue().cancel(this); save(); } public void disable() throws IOException { makeDisabled(true); } public void enable() throws IOException { makeDisabled(false); } @Override public BallColor getIconColor() { if(isDisabled()) return BallColor.DISABLED; else return super.getIconColor(); } /** * effectively deprecated. Since using updateTransientActions correctly * under concurrent environment requires a lock that can too easily cause deadlocks. * * <p> * Override {@link #createTransientActions()} instead. */ protected void updateTransientActions() { transientActions = createTransientActions(); } protected List<Action> createTransientActions() { Vector<Action> ta = new Vector<Action>(); for (JobProperty<? super P> p : getAllProperties()) ta.addAll(p.getJobActions((P)this)); for (TransientProjectActionFactory tpaf : TransientProjectActionFactory.all()) ta.addAll(Util.fixNull(tpaf.createFor(this))); // be defensive against null return ta; } /** * Returns the live list of all {@link Publisher}s configured for this project. * * <p> * This method couldn't be called <tt>getPublishers()</tt> because existing methods * in sub-classes return different inconsistent types. */ public abstract DescribableList<Publisher,Descriptor<Publisher>> getPublishersList(); @Override public void addProperty(JobProperty<? super P> jobProp) throws IOException { super.addProperty(jobProp); updateTransientActions(); } public List<ProminentProjectAction> getProminentActions() { List<Action> a = getActions(); List<ProminentProjectAction> pa = new Vector<ProminentProjectAction>(); for (Action action : a) { if(action instanceof ProminentProjectAction) pa.add((ProminentProjectAction) action); } return pa; } @Override public void doConfigSubmit( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException, FormException { super.doConfigSubmit(req,rsp); updateTransientActions(); Set<AbstractProject> upstream = Collections.emptySet(); if(req.getParameter("pseudoUpstreamTrigger")!=null) { upstream = new HashSet<AbstractProject>(Items.fromNameList(req.getParameter("upstreamProjects"),AbstractProject.class)); } // dependency setting might have been changed by the user, so rebuild. Hudson.getInstance().rebuildDependencyGraph(); // reflect the submission of the pseudo 'upstream build trriger'. // this needs to be done after we release the lock on 'this', // or otherwise we could dead-lock for (AbstractProject<?,?> p : Hudson.getInstance().getAllItems(AbstractProject.class)) { // Don't consider child projects such as MatrixConfiguration: if (!p.isConfigurable()) continue; boolean isUpstream = upstream.contains(p); synchronized(p) { // does 'p' include us in its BuildTrigger? DescribableList<Publisher,Descriptor<Publisher>> pl = p.getPublishersList(); BuildTrigger trigger = pl.get(BuildTrigger.class); List<AbstractProject> newChildProjects = trigger == null ? new ArrayList<AbstractProject>():trigger.getChildProjects(); if(isUpstream) { if(!newChildProjects.contains(this)) newChildProjects.add(this); } else { newChildProjects.remove(this); } if(newChildProjects.isEmpty()) { pl.remove(BuildTrigger.class); } else { // here, we just need to replace the old one with the new one, // but there was a regression (we don't know when it started) that put multiple BuildTriggers // into the list. // for us not to lose the data, we need to merge them all. List<BuildTrigger> existingList = pl.getAll(BuildTrigger.class); BuildTrigger existing; switch (existingList.size()) { case 0: existing = null; break; case 1: existing = existingList.get(0); break; default: pl.removeAll(BuildTrigger.class); Set<AbstractProject> combinedChildren = new HashSet<AbstractProject>(); for (BuildTrigger bt : existingList) combinedChildren.addAll(bt.getChildProjects()); existing = new BuildTrigger(new ArrayList<AbstractProject>(combinedChildren),existingList.get(0).getThreshold()); pl.add(existing); break; } if(existing!=null && existing.hasSame(newChildProjects)) continue; // no need to touch pl.replace(new BuildTrigger(newChildProjects, existing==null?Result.SUCCESS:existing.getThreshold())); } BuildTrigger buildTrigger = pl.get(BuildTrigger.class); CascadingUtil.getExternalProjectProperty(p, BUILD_TRIGGER_PROPERTY_NAME).setValue(buildTrigger); } } // notify the queue as the project might be now tied to different node Hudson.getInstance().getQueue().scheduleMaintenance(); // this is to reflect the upstream build adjustments done above Hudson.getInstance().rebuildDependencyGraph(); } /** * @deprecated * Use {@link #scheduleBuild(Cause)}. Since 1.283 */ public boolean scheduleBuild() { return scheduleBuild(new LegacyCodeCause()); } /** * @deprecated * Use {@link #scheduleBuild(int, Cause)}. Since 1.283 */ public boolean scheduleBuild(int quietPeriod) { return scheduleBuild(quietPeriod, new LegacyCodeCause()); } /** * Schedules a build of this project. * * @return * true if the project is actually added to the queue. * false if the queue contained it and therefore the add() * was noop */ public boolean scheduleBuild(Cause c) { return scheduleBuild(getQuietPeriod(), c); } public boolean scheduleBuild(int quietPeriod, Cause c) { return scheduleBuild(quietPeriod, c, new Action[0]); } /** * Schedules a build. * * Important: the actions should be persistable without outside references (e.g. don't store * references to this project). To provide parameters for a parameterized project, add a ParametersAction. If * no ParametersAction is provided for such a project, one will be created with the default parameter values. * * @param quietPeriod the quiet period to observer * @param c the cause for this build which should be recorded * @param actions a list of Actions that will be added to the build * @return whether the build was actually scheduled */ public boolean scheduleBuild(int quietPeriod, Cause c, Action... actions) { return scheduleBuild2(quietPeriod,c,actions)!=null; } /** * Schedules a build of this project, and returns a {@link Future} object * to wait for the completion of the build. * * @param actions * For the convenience of the caller, this array can contain null, and those will be silently ignored. */ public Future<R> scheduleBuild2(int quietPeriod, Cause c, Action... actions) { return scheduleBuild2(quietPeriod, c, Arrays.asList(actions)); } /** * Schedules a build of this project, and returns a {@link Future} object * to wait for the completion of the build. * * @param actions * For the convenience of the caller, this collection can contain null, and those will be silently ignored. * @since 1.383 */ public Future<R> scheduleBuild2(int quietPeriod, Cause c, Collection<? extends Action> actions) { if (!isBuildable()) return null; List<Action> queueActions = new ArrayList<Action>(actions); if (isParameterized() && Util.filter(queueActions, ParametersAction.class).isEmpty()) { queueActions.add(new ParametersAction(getDefaultParametersValues())); } if (c != null) { queueActions.add(new CauseAction(c)); } WaitingItem i = Hudson.getInstance().getQueue().schedule(this, quietPeriod, queueActions); if(i!=null) return (Future)i.getFuture(); return null; } private List<ParameterValue> getDefaultParametersValues() { ParametersDefinitionProperty paramDefProp = getProperty(ParametersDefinitionProperty.class); ArrayList<ParameterValue> defValues = new ArrayList<ParameterValue>(); /* * This check is made ONLY if someone will call this method even if isParametrized() is false. */ if(paramDefProp == null) return defValues; /* Scan for all parameter with an associated default values */ for(ParameterDefinition paramDefinition : paramDefProp.getParameterDefinitions()) { ParameterValue defaultValue = paramDefinition.getDefaultParameterValue(); if(defaultValue != null) defValues.add(defaultValue); } return defValues; } /** * Schedules a build, and returns a {@link Future} object * to wait for the completion of the build. * * <p> * Production code shouldn't be using this, but for tests this is very convenient, so this isn't marked * as deprecated. */ public Future<R> scheduleBuild2(int quietPeriod) { return scheduleBuild2(quietPeriod, new LegacyCodeCause()); } /** * Schedules a build of this project, and returns a {@link Future} object * to wait for the completion of the build. */ public Future<R> scheduleBuild2(int quietPeriod, Cause c) { return scheduleBuild2(quietPeriod, c, new Action[0]); } /** * Schedules a polling of this project. */ public boolean schedulePolling() { if(isDisabled()) return false; SCMTrigger scmt = getTrigger(SCMTrigger.class); if(scmt==null) return false; scmt.run(); return true; } /** * Returns true if the build is in the queue. */ @Override public boolean isInQueue() { return Hudson.getInstance().getQueue().contains(this); } @Override public Queue.Item getQueueItem() { return Hudson.getInstance().getQueue().getItem(this); } /** * @return name of jdk chosen for current project. Could taken from parent */ public String getJDKName() { return CascadingUtil.getStringProjectProperty(this, JDK_PROPERTY_NAME).getValue(); } /** * @return JDK that this project is configured with, or null. */ public JDK getJDK() { return Hudson.getInstance().getJDK(getJDKName()); } /** * Overwrites the JDK setting. */ public void setJDK(JDK jdk) throws IOException { setJDK(jdk.getName()); save(); } public void setJDK(String jdk) { CascadingUtil.getStringProjectProperty(this, JDK_PROPERTY_NAME).setValue(jdk); } public BuildAuthorizationToken getAuthToken() { return authToken; } @Override public SortedMap<Integer, ? extends R> _getRuns() { return builds.getView(); } @Override public void removeRun(R run) { this.builds.remove(run); } /** * Determines Class<R>. */ protected abstract Class<R> getBuildClass(); // keep track of the previous time we started a build private transient long lastBuildStartTime; /** * Creates a new build of this project for immediate execution. */ protected synchronized R newBuild() throws IOException { // make sure we don't start two builds in the same second // so the build directories will be different too long timeSinceLast = System.currentTimeMillis() - lastBuildStartTime; if (timeSinceLast < 1000) { try { Thread.sleep(1000 - timeSinceLast); } catch (InterruptedException e) { } } lastBuildStartTime = System.currentTimeMillis(); try { R lastBuild = getBuildClass().getConstructor(getClass()).newInstance(this); builds.put(lastBuild); return lastBuild; } catch (InstantiationException e) { throw new Error(e); } catch (IllegalAccessException e) { throw new Error(e); } catch (InvocationTargetException e) { throw handleInvocationTargetException(e); } catch (NoSuchMethodException e) { throw new Error(e); } } private IOException handleInvocationTargetException(InvocationTargetException e) { Throwable t = e.getTargetException(); if(t instanceof Error) throw (Error)t; if(t instanceof RuntimeException) throw (RuntimeException)t; if(t instanceof IOException) return (IOException)t; throw new Error(t); } /** * Loads an existing build record from disk. */ protected R loadBuild(File dir) throws IOException { try { return getBuildClass().getConstructor(getClass(),File.class).newInstance(this,dir); } catch (InstantiationException e) { throw new Error(e); } catch (IllegalAccessException e) { throw new Error(e); } catch (InvocationTargetException e) { throw handleInvocationTargetException(e); } catch (NoSuchMethodException e) { throw new Error(e); } } /** * {@inheritDoc} * * <p> * Note that this method returns a read-only view of {@link Action}s. * {@link BuildStep}s and others who want to add a project action * should do so by implementing {@link BuildStep#getProjectActions(AbstractProject)}. * * @see TransientProjectActionFactory */ @Override public synchronized List<Action> getActions() { // add all the transient actions, too List<Action> actions = new Vector<Action>(super.getActions()); actions.addAll(transientActions); // return the read only list to cause a failure on plugins who try to add an action here return Collections.unmodifiableList(actions); } /** * Gets the {@link Node} where this project was last built on. * * @return * null if no information is available (for example, * if no build was done yet.) */ public Node getLastBuiltOn() { // where was it built on? AbstractBuild b = getLastBuild(); if(b==null) return null; else return b.getBuiltOn(); } public Object getSameNodeConstraint() { return this; // in this way, any member that wants to run with the main guy can nominate the project itself } public final Task getOwnerTask() { return this; } /** * {@inheritDoc} * * <p> * A project must be blocked if its own previous build is in progress, * or if the blockBuildWhenUpstreamBuilding option is true and an upstream * project is building, but derived classes can also check other conditions. */ public boolean isBuildBlocked() { return getCauseOfBlockage()!=null; } public String getWhyBlocked() { CauseOfBlockage cb = getCauseOfBlockage(); return cb!=null ? cb.getShortDescription() : null; } /** * Blocked because the previous build is already in progress. */ public static class BecauseOfBuildInProgress extends CauseOfBlockage { private final AbstractBuild<?,?> build; public BecauseOfBuildInProgress(AbstractBuild<?, ?> build) { this.build = build; } @Override public String getShortDescription() { Executor e = build.getExecutor(); String eta = ""; if (e != null) eta = Messages.AbstractProject_ETA(e.getEstimatedRemainingTime()); int lbn = build.getNumber(); return Messages.AbstractProject_BuildInProgress(lbn, eta); } } /** * Because the downstream build is in progress, and we are configured to wait for that. */ public static class BecauseOfDownstreamBuildInProgress extends CauseOfBlockage { //TODO: review and check whether we can do it private public final AbstractProject<?,?> up; public AbstractProject getUp() { return up; } public BecauseOfDownstreamBuildInProgress(AbstractProject<?,?> up) { this.up = up; } @Override public String getShortDescription() { return Messages.AbstractProject_DownstreamBuildInProgress(up.getName()); } } /** * Because the upstream build is in progress, and we are configured to wait for that. */ public static class BecauseOfUpstreamBuildInProgress extends CauseOfBlockage { //TODO: review and check whether we can do it private public final AbstractProject<?,?> up; public BecauseOfUpstreamBuildInProgress(AbstractProject<?,?> up) { this.up = up; } public AbstractProject getUp() { return up; } @Override public String getShortDescription() { return Messages.AbstractProject_UpstreamBuildInProgress(up.getName()); } } public CauseOfBlockage getCauseOfBlockage() { if (isBuilding() && !isConcurrentBuild()) return new BecauseOfBuildInProgress(getLastBuild()); if (blockBuildWhenDownstreamBuilding()) { AbstractProject<?,?> bup = getBuildingDownstream(); if (bup!=null) return new BecauseOfDownstreamBuildInProgress(bup); } if (blockBuildWhenUpstreamBuilding()) { AbstractProject<?,?> bup = getBuildingUpstream(); if (bup!=null) return new BecauseOfUpstreamBuildInProgress(bup); } return null; } /** * Returns the project if any of the downstream project (or itself) is either * building or is in the unblocked queue. * <p> * This means eventually there will be an automatic triggering of * the given project (provided that all builds went smoothly.) */ protected AbstractProject getBuildingDownstream() { DependencyGraph graph = Hudson.getInstance().getDependencyGraph(); Set<AbstractProject> tups = graph.getTransitiveDownstream(this); tups.add(this); for (AbstractProject tup : tups) { if(tup!=this && (tup.isBuilding() || tup.isInUnblockedQueue())) return tup; } return null; } /** * Returns the project if any of the upstream project (or itself) is either * building or is in the unblocked queue. * <p> * This means eventually there will be an automatic triggering of * the given project (provided that all builds went smoothly.) */ protected AbstractProject getBuildingUpstream() { DependencyGraph graph = Hudson.getInstance().getDependencyGraph(); Set<AbstractProject> tups = graph.getTransitiveUpstream(this); tups.add(this); for (AbstractProject tup : tups) { if(tup!=this && (tup.isBuilding() || tup.isInUnblockedQueue())) return tup; } return null; } private boolean isInUnblockedQueue() { return Hudson.getInstance().getQueue().getUnblockedQueuedTasks().contains(this); } public List<SubTask> getSubTasks() { List<SubTask> r = new ArrayList<SubTask>(); r.add(this); for (SubTaskContributor euc : SubTaskContributor.all()) r.addAll(euc.forProject(this)); for (JobProperty<? super P> p : getAllProperties()) r.addAll(p.getSubTasks()); return r; } public R createExecutable() throws IOException { if(isDisabled()) return null; return newBuild(); } public void checkAbortPermission() { checkPermission(AbstractProject.ABORT); } public boolean hasAbortPermission() { return hasPermission(AbstractProject.ABORT); } /** * Gets the {@link Resource} that represents the workspace of this project. * Useful for locking and mutual exclusion control. * * @deprecated as of 1.319 * Projects no longer have a fixed workspace, ands builds will find an available workspace via * {@link WorkspaceList} for each build (furthermore, that happens after a build is started.) * So a {@link Resource} representation for a workspace at the project level no longer makes sense. * * <p> * If you need to lock a workspace while you do some computation, see the source code of * {@link #pollSCMChanges(TaskListener)} for how to obtain a lock of a workspace through {@link WorkspaceList}. */ public Resource getWorkspaceResource() { return new Resource(getFullDisplayName()+" workspace"); } /** * List of necessary resources to perform the build of this project. */ public ResourceList getResourceList() { final Set<ResourceActivity> resourceActivities = getResourceActivities(); final List<ResourceList> resourceLists = new ArrayList<ResourceList>(1 + resourceActivities.size()); for (ResourceActivity activity : resourceActivities) { if (activity != this && activity != null) { // defensive infinite recursion and null check resourceLists.add(activity.getResourceList()); } } return ResourceList.union(resourceLists); } /** * Set of child resource activities of the build of this project (override in child projects). * @return The set of child resource activities of the build of this project. */ protected Set<ResourceActivity> getResourceActivities() { return Collections.emptySet(); } public boolean checkout(AbstractBuild build, Launcher launcher, BuildListener listener, File changelogFile) throws IOException, InterruptedException { SCM scm = getScm(); if(scm==null) return true; // no SCM FilePath workspace = build.getWorkspace(); workspace.mkdirs(); boolean r = scm.checkout(build, launcher, workspace, listener, changelogFile); calcPollingBaseline(build, launcher, listener); return r; } /** * Pushes the baseline up to the newly checked out revision. */ private void calcPollingBaseline(AbstractBuild build, Launcher launcher, TaskListener listener) throws IOException, InterruptedException { SCMRevisionState baseline = build.getAction(SCMRevisionState.class); if (baseline==null) { try { baseline = getScm()._calcRevisionsFromBuild(build, launcher, listener); } catch (AbstractMethodError e) { baseline = SCMRevisionState.NONE; // pre-1.345 SCM implementations, which doesn't use the baseline in polling } if (baseline!=null) build.addAction(baseline); } pollingBaseline = baseline; } /** * For reasons I don't understand, if I inline this method, AbstractMethodError escapes try/catch block. */ private SCMRevisionState safeCalcRevisionsFromBuild(AbstractBuild build, Launcher launcher, TaskListener listener) throws IOException, InterruptedException { return getScm()._calcRevisionsFromBuild(build, launcher, listener); } /** * Checks if there's any update in SCM, and returns true if any is found. * * @deprecated as of 1.346 * Use {@link #poll(TaskListener)} instead. */ public boolean pollSCMChanges( TaskListener listener ) { return poll(listener).hasChanges(); } /** * Checks if there's any update in SCM, and returns true if any is found. * * <p> * The implementation is responsible for ensuring mutual exclusion between polling and builds * if necessary. * * @since 1.345 */ public PollingResult poll( TaskListener listener ) { SCM scm = getScm(); if (scm==null) { listener.getLogger().println(Messages.AbstractProject_NoSCM()); return NO_CHANGES; } if (isDisabled()) { listener.getLogger().println(Messages.AbstractProject_Disabled()); return NO_CHANGES; } R lb = getLastBuild(); if (lb==null) { listener.getLogger().println(Messages.AbstractProject_NoBuilds()); return isInQueue() ? NO_CHANGES : BUILD_NOW; } if (pollingBaseline==null) { R success = getLastSuccessfulBuild(); // if we have a persisted baseline, we'll find it by this for (R r=lb; r!=null; r=r.getPreviousBuild()) { SCMRevisionState s = r.getAction(SCMRevisionState.class); if (s!=null) { pollingBaseline = s; break; } if (r==success) break; // searched far enough } // NOTE-NO-BASELINE: // if we don't have baseline yet, it means the data is built by old Hudson that doesn't set the baseline // as action, so we need to compute it. This happens later. } try { if (scm.requiresWorkspaceForPolling()) { // lock the workspace of the last build FilePath ws=lb.getWorkspace(); if (ws==null || !ws.exists()) { // workspace offline. build now, or nothing will ever be built Label label = getAssignedLabel(); if (label != null && label.isSelfLabel()) { // if the build is fixed on a node, then attempting a build will do us // no good. We should just wait for the slave to come back. listener.getLogger().println(Messages.AbstractProject_NoWorkspace()); return NO_CHANGES; } listener.getLogger().println( ws==null ? Messages.AbstractProject_WorkspaceOffline() : Messages.AbstractProject_NoWorkspace()); if (isInQueue()) { listener.getLogger().println(Messages.AbstractProject_AwaitingBuildForWorkspace()); return NO_CHANGES; } else { listener.getLogger().println(Messages.AbstractProject_NewBuildForWorkspace()); return BUILD_NOW; } } else { Node node = lb.getBuiltOn(); if (node == null || node.toComputer() == null) { LOGGER.log(Level.FINE, "Node on which this job previously was built is not available now, build is started on an available node"); return isInQueue() ? NO_CHANGES : BUILD_NOW; } WorkspaceList l = node.toComputer().getWorkspaceList(); // if doing non-concurrent build, acquire a workspace in a way that causes builds to block for this workspace. // this prevents multiple workspaces of the same job --- the behavior of Hudson < 1.319. // // OTOH, if a concurrent build is chosen, the user is willing to create a multiple workspace, // so better throughput is achieved over time (modulo the initial cost of creating that many workspaces) // by having multiple workspaces WorkspaceList.Lease lease = l.acquire(ws, !concurrentBuild); Launcher launcher = ws.createLauncher(listener); try { LOGGER.fine("Polling SCM changes of " + getName()); if (pollingBaseline==null) // see NOTE-NO-BASELINE above calcPollingBaseline(lb,launcher,listener); PollingResult r = scm.poll(this, launcher, ws, listener, pollingBaseline); pollingBaseline = r.remote; return r; } finally { lease.release(); } } } else { // polling without workspace LOGGER.fine("Polling SCM changes of " + getName()); if (pollingBaseline==null) // see NOTE-NO-BASELINE above calcPollingBaseline(lb,null,listener); PollingResult r = scm.poll(this, null, null, listener, pollingBaseline); pollingBaseline = r.remote; return r; } } catch (AbortException e) { listener.getLogger().println(e.getMessage()); listener.fatalError(Messages.AbstractProject_Aborted()); LOGGER.log(Level.FINE, "Polling "+this+" aborted",e); return NO_CHANGES; } catch (IOException e) { e.printStackTrace(listener.fatalError(e.getMessage())); return NO_CHANGES; } catch (InterruptedException e) { e.printStackTrace(listener.fatalError(Messages.AbstractProject_PollingABorted())); return NO_CHANGES; } } /** * Returns true if this user has made a commit to this project. * * @since 1.191 */ public boolean hasParticipant(User user) { for( R build = getLastBuild(); build!=null; build=build.getPreviousBuild()) if(build.hasParticipant(user)) return true; return false; } @Exported public SCM getScm() { return CascadingUtil.getScmProjectProperty(this, SCM_PROPERTY_NAME).getValue(); } @SuppressWarnings("unchecked") public void setScm(SCM scm) throws IOException { CascadingUtil.getScmProjectProperty(this, SCM_PROPERTY_NAME).setValue(scm); save(); } /** * Adds a new {@link Trigger} to this {@link Project} if not active yet. */ @SuppressWarnings("unchecked") public void addTrigger(Trigger<?> trigger) throws IOException { CascadingUtil.getTriggerProjectProperty(this, trigger.getDescriptor().getJsonSafeClassName()).setValue(trigger); save(); updateTransientActions(); } @SuppressWarnings("unchecked") public void removeTrigger(TriggerDescriptor trigger) throws IOException { CascadingUtil.getTriggerProjectProperty(this, trigger.getJsonSafeClassName()).setValue(null); save(); updateTransientActions(); } protected final synchronized <T extends Describable<T>> void addToList( T item, List<T> collection ) throws IOException { for( int i=0; i<collection.size(); i++ ) { if(collection.get(i).getDescriptor()==item.getDescriptor()) { // replace collection.set(i, item); save(); return; } } // add collection.add(item); save(); updateTransientActions(); } protected final synchronized <T extends Describable<T>> void removeFromList(Descriptor<T> item, List<T> collection) throws IOException { for( int i=0; i< collection.size(); i++ ) { if(collection.get(i).getDescriptor()==item) { // found it collection.remove(i); save(); updateTransientActions(); return; } } } @SuppressWarnings("unchecked") public synchronized Map<TriggerDescriptor,Trigger> getTriggers() { return (Map)Descriptor.toMap(getTriggerDescribableList()); } /** * @return list of {@link Trigger} elements. */ public List<Trigger<?>> getTriggersList() { return getTriggerDescribableList().toList(); } /** * @return describable list of trigger elements. */ public DescribableList<Trigger<?>, TriggerDescriptor> getTriggerDescribableList() { return DescribableListUtil.convertToDescribableList(Trigger.for_(this), this, TriggerProjectProperty.class); } /** * Gets the specific trigger, or null if the propert is not configured for this job. */ public <T extends Trigger> T getTrigger(Class<T> clazz) { for (Trigger p : getTriggersList()) { if(clazz.isInstance(p)) return clazz.cast(p); } return null; } @SuppressWarnings("unchecked") public void setTriggers(List<Trigger<?>> triggerList) { for (Trigger trigger : triggerList) { CascadingUtil.getTriggerProjectProperty(this, trigger.getDescriptor().getJsonSafeClassName()).setValue(trigger); } } // // // fingerprint related // // /** * True if the builds of this project produces {@link Fingerprint} records. */ public abstract boolean isFingerprintConfigured(); /** * Gets the other {@link AbstractProject}s that should be built * when a build of this project is completed. */ @Exported public final List<AbstractProject> getDownstreamProjects() { return Hudson.getInstance().getDependencyGraph().getDownstream(this); } @Exported public final List<AbstractProject> getUpstreamProjects() { return Hudson.getInstance().getDependencyGraph().getUpstream(this); } /** * Returns only those upstream projects that defines {@link BuildTrigger} to this project. * This is a subset of {@link #getUpstreamProjects()} * * @return A List of upstream projects that has a {@link BuildTrigger} to this project. */ public final List<AbstractProject> getBuildTriggerUpstreamProjects() { ArrayList<AbstractProject> result = new ArrayList<AbstractProject>(); for (AbstractProject<?,?> ap : getUpstreamProjects()) { BuildTrigger buildTrigger = ap.getPublishersList().get(BuildTrigger.class); if (buildTrigger != null) if (buildTrigger.getChildProjects().contains(this)) result.add(ap); } return result; } /** * Gets all the upstream projects including transitive upstream projects. * * @since 1.138 */ public final Set<AbstractProject> getTransitiveUpstreamProjects() { return Hudson.getInstance().getDependencyGraph().getTransitiveUpstream(this); } /** * Gets all the downstream projects including transitive downstream projects. * * @since 1.138 */ public final Set<AbstractProject> getTransitiveDownstreamProjects() { return Hudson.getInstance().getDependencyGraph().getTransitiveDownstream(this); } /** * Gets the dependency relationship map between this project (as the source) * and that project (as the sink.) * * @return * can be empty but not null. build number of this project to the build * numbers of that project. */ public SortedMap<Integer, RangeSet> getRelationship(AbstractProject that) { TreeMap<Integer,RangeSet> r = new TreeMap<Integer,RangeSet>(REVERSE_INTEGER_COMPARATOR); checkAndRecord(that, r, this.getBuilds()); // checkAndRecord(that, r, that.getBuilds()); return r; } /** * Helper method for getDownstreamRelationship. * * For each given build, find the build number range of the given project and put that into the map. */ private void checkAndRecord(AbstractProject that, TreeMap<Integer, RangeSet> r, Collection<R> builds) { for (R build : builds) { RangeSet rs = build.getDownstreamRelationship(that); if(rs==null || rs.isEmpty()) continue; int n = build.getNumber(); RangeSet value = r.get(n); if(value==null) r.put(n,rs); else value.add(rs); } } /** * Builds the dependency graph. * @see DependencyGraph */ protected abstract void buildDependencyGraph(DependencyGraph graph); @Override protected SearchIndexBuilder makeSearchIndex() { SearchIndexBuilder sib = super.makeSearchIndex(); if(isBuildable() && hasPermission(Hudson.ADMINISTER)) sib.add("build","build"); return sib; } @Override protected HistoryWidget createHistoryWidget() { return new BuildHistoryWidget<R>(this,getBuilds(),HISTORY_ADAPTER); } public boolean isParameterized() { return getProperty(ParametersDefinitionProperty.class) != null; } // // // actions // // /** * Schedules a new build command. */ public void doBuild( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { BuildAuthorizationToken.checkPermission(this, authToken, req, rsp); // if a build is parameterized, let that take over ParametersDefinitionProperty pp = getProperty(ParametersDefinitionProperty.class); if (pp != null) { pp._doBuild(req,rsp); return; } if (!isBuildable()) throw HttpResponses.error(SC_INTERNAL_SERVER_ERROR,new IOException(getFullName()+" is not buildable")); Hudson.getInstance().getQueue().schedule(this, getDelay(req), getBuildCause(req)); rsp.forwardToPreviousPage(req); } /** * Computes the build cause, using RemoteCause or UserCause as appropriate. */ /*package*/ CauseAction getBuildCause(StaplerRequest req) { Cause cause; if (authToken != null && authToken.getToken() != null && req.getParameter("token") != null) { // Optional additional cause text when starting via token String causeText = req.getParameter("cause"); cause = new RemoteCause(req.getRemoteAddr(), causeText); } else { cause = new UserCause(); } return new CauseAction(cause); } /** * Computes the delay by taking the default value and the override in the request parameter into the account. */ public int getDelay(StaplerRequest req) throws ServletException { String delay = req.getParameter("delay"); if (delay==null) return getQuietPeriod(); try { // TODO: more unit handling if(delay.endsWith("sec")) delay=delay.substring(0,delay.length()-3); if(delay.endsWith("secs")) delay=delay.substring(0,delay.length()-4); return Integer.parseInt(delay); } catch (NumberFormatException e) { throw new ServletException("Invalid delay parameter value: "+delay); } } /** * Supports build trigger with parameters via an HTTP GET or POST. * Currently only String parameters are supported. */ public void doBuildWithParameters(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { BuildAuthorizationToken.checkPermission(this, authToken, req, rsp); ParametersDefinitionProperty pp = getProperty(ParametersDefinitionProperty.class); if (pp != null) { pp.buildWithParameters(req,rsp); } else { throw new IllegalStateException("This build is not parameterized!"); } } /** * Schedules a new SCM polling command. */ public void doPolling( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { BuildAuthorizationToken.checkPermission(this, authToken, req, rsp); schedulePolling(); rsp.forwardToPreviousPage(req); } /** * Cancels a scheduled build. */ public void doCancelQueue( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { checkPermission(BUILD); Hudson.getInstance().getQueue().cancel(this); rsp.forwardToPreviousPage(req); } @Override protected void submit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, FormException { super.submit(req,rsp); makeDisabled(null != req.getParameter("disable")); setJDK(req.getParameter("jdk")); setQuietPeriod(null != req.getParameter(HAS_QUIET_PERIOD_PROPERTY_NAME) ? req.getParameter("quiet_period") : null); setScmCheckoutRetryCount(null != req.getParameter(HAS_SCM_CHECKOUT_RETRY_COUNT_PROPERTY_NAME) ? req.getParameter("scmCheckoutRetryCount") : null); setBlockBuildWhenDownstreamBuilding(null != req.getParameter("blockBuildWhenDownstreamBuilding")); setBlockBuildWhenUpstreamBuilding(null != req.getParameter("blockBuildWhenUpstreamBuilding")); if (req.getParameter(APPOINTED_NODE_PROPERTY_NAME) != null) { // New logic for handling whether this choice came from the dropdown or textfield. if (BASIC_KEY.equals(req.getParameter(AFFINITY_CHO0SER_KEY))) { setAppointedNode( new AppointedNode(Util.fixEmptyAndTrim(req.getParameter(SLAVE_KEY)), false)); } else { setAppointedNode( new AppointedNode(Util.fixEmptyAndTrim(req.getParameter(ASSIGNED_LABEL_KEY)), true)); } } else { setAppointedNode(null); } setCleanWorkspaceRequired(null != req.getParameter("cleanWorkspaceRequired")); setConcurrentBuild(req.getSubmittedForm().has("concurrentBuild")); authToken = BuildAuthorizationToken.create(req); setScm(SCMS.parseSCM(req,this)); buildTriggers(req, req.getSubmittedForm(), Trigger.for_(this)); } /** * @deprecated * As of 1.261. Use {@link #buildDescribable(StaplerRequest, List)} instead. */ protected final <T extends Describable<T>> List<T> buildDescribable(StaplerRequest req, List<? extends Descriptor<T>> descriptors, String prefix) throws FormException, ServletException { return buildDescribable(req,descriptors); } protected final <T extends Describable<T>> List<T> buildDescribable(StaplerRequest req, List<? extends Descriptor<T>> descriptors) throws FormException, ServletException { JSONObject data = req.getSubmittedForm(); List<T> r = new Vector<T>(); for (Descriptor<T> d : descriptors) { String safeName = d.getJsonSafeClassName(); if (req.getParameter(safeName) != null) { T instance = d.newInstance(req, data.getJSONObject(safeName)); r.add(instance); } } return r; } protected void buildTriggers(StaplerRequest req, JSONObject json, List<TriggerDescriptor> descriptors) throws FormException { for (TriggerDescriptor d : descriptors) { String propertyName = d.getJsonSafeClassName(); CascadingUtil.setChildrenTrigger(this, d, propertyName, req, json); } } /** * Serves the workspace files. */ public DirectoryBrowserSupport doWs( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException, InterruptedException { checkPermission(AbstractProject.WORKSPACE); FilePath ws = getSomeWorkspace(); if ((ws == null) || (!ws.exists())) { // if there's no workspace, report a nice error message rsp.setStatus(HttpServletResponse.SC_NOT_FOUND); req.getView(this,"noWorkspace.jelly").forward(req,rsp); return null; } else { return new DirectoryBrowserSupport(this, ws, getDisplayName()+" workspace", "folder.gif", true); } } /** * Wipes out the workspace. */ public HttpResponse doDoWipeOutWorkspace() throws IOException, ServletException, InterruptedException { checkPermission(Functions.isWipeOutPermissionEnabled() ? WIPEOUT : BUILD); try { if (cleanWorkspace()) { return new HttpRedirect("."); } else { return new ForwardToView(this,"wipeOutWorkspaceBlocked.jelly"); } } catch (IOException e) { ForwardToView resp = new ForwardToView(this, "error.jelly"); resp.with("message", Messages._AbstractProject_UnableWipeOut()); resp.with("pre", false); resp.with("exception", e); return resp; } } public boolean cleanWorkspace() throws IOException, InterruptedException{ checkPermission(BUILD); R b = getSomeBuildWithWorkspace(); FilePath ws = b != null ? b.getWorkspace() : null; if (ws != null && getScm().processWorkspaceBeforeDeletion(this, ws, b.getBuiltOn())) { ws.deleteRecursive(); return true; } else{ // If we get here, that means the SCM blocked the workspace deletion. return false; } } @CLIMethod(name="disable-job") public HttpResponse doDisable() throws IOException, ServletException { requirePOST(); checkPermission(CONFIGURE); makeDisabled(true); return new HttpRedirect("."); } @CLIMethod(name="enable-job") public HttpResponse doEnable() throws IOException, ServletException { requirePOST(); checkPermission(CONFIGURE); makeDisabled(false); return new HttpRedirect("."); } /** * RSS feed for changes in this project. */ public void doRssChangelog( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException { class FeedItem { ChangeLogSet.Entry e; int idx; public FeedItem(Entry e, int idx) { this.e = e; this.idx = idx; } AbstractBuild<?,?> getBuild() { return e.getParent().build; } } List<FeedItem> entries = new ArrayList<FeedItem>(); for(R r=getLastBuild(); r!=null; r=r.getPreviousBuild()) { int idx=0; for( ChangeLogSet.Entry e : r.getChangeSet()) entries.add(new FeedItem(e,idx++)); } RSS.forwardToRss( getDisplayName()+' '+getScm().getDescriptor().getDisplayName()+" changes", getUrl()+"changes", entries, new FeedAdapter<FeedItem>() { public String getEntryTitle(FeedItem item) { return "#"+item.getBuild().number+' '+item.e.getMsg()+" ("+item.e.getAuthor()+")"; } public String getEntryUrl(FeedItem item) { return item.getBuild().getUrl()+"changes#detail"+item.idx; } public String getEntryID(FeedItem item) { return getEntryUrl(item); } public String getEntryDescription(FeedItem item) { StringBuilder buf = new StringBuilder(); for(String path : item.e.getAffectedPaths()) buf.append(path).append('\n'); return buf.toString(); } public Calendar getEntryTimestamp(FeedItem item) { return item.getBuild().getTimestamp(); } public String getEntryAuthor(FeedItem entry) { return Mailer.descriptor().getAdminAddress(); } }, req, rsp ); } /** * {@link AbstractProject} subtypes should implement this base class as a descriptor. * * @since 1.294 */ public static abstract class AbstractProjectDescriptor extends TopLevelItemDescriptor { /** * {@link AbstractProject} subtypes can override this method to veto some {@link Descriptor}s * from showing up on their configuration screen. This is often useful when you are building * a workflow/company specific project type, where you want to limit the number of choices * given to the users. * * <p> * Some {@link Descriptor}s define their own schemes for controlling applicability * (such as {@link BuildStepDescriptor#isApplicable(Class)}), * This method works like AND in conjunction with them; * Both this method and that method need to return true in order for a given {@link Descriptor} * to show up for the given {@link Project}. * * <p> * The default implementation returns true for everything. * * @see BuildStepDescriptor#isApplicable(Class) * @see BuildWrapperDescriptor#isApplicable(AbstractProject) * @see TriggerDescriptor#isApplicable(Item) */ @Override public boolean isApplicable(Descriptor descriptor) { return true; } public FormValidation doCheckAssignedLabelString(@QueryParameter String value) { if (Util.fixEmpty(value)==null) return FormValidation.ok(); // nothing typed yet try { Label.parseExpression(value); } catch (ANTLRException e) { return FormValidation.error(e, Messages.AbstractProject_AssignedLabelString_InvalidBooleanExpression(e.getMessage())); } // TODO: if there's an atom in the expression that is empty, report it if (Hudson.getInstance().getLabel(value).isEmpty()) return FormValidation.warning(Messages.AbstractProject_AssignedLabelString_NoMatch()); return FormValidation.ok(); } public AutoCompletionCandidates doAutoCompleteAssignedLabelString(@QueryParameter String value) { AutoCompletionCandidates c = new AutoCompletionCandidates(); Set<Label> labels = Hudson.getInstance().getLabels(); List<String> queries = new AutoCompleteSeeder(value).getSeeds(); for (String term : queries) { for (Label l : labels) { if (l.getName().startsWith(term)) { c.add(l.getName()); } } } return c; } } /** * Finds a {@link AbstractProject} that has the name closest to the given name. */ public static AbstractProject findNearest(String name) { List<AbstractProject> projects = Hudson.getInstance().getItems(AbstractProject.class); String[] names = new String[projects.size()]; for( int i=0; i<projects.size(); i++ ) names[i] = projects.get(i).getName(); String nearest = EditDistance.findNearest(name, names); return (AbstractProject)Hudson.getInstance().getItem(nearest); } private static final Comparator<Integer> REVERSE_INTEGER_COMPARATOR = new Comparator<Integer>() { public int compare(Integer o1, Integer o2) { return o2-o1; } }; private static final Logger LOGGER = Logger.getLogger(AbstractProject.class.getName()); /** * Permission to abort a build. For now, let's make it the same as {@link #BUILD} */ public static final Permission ABORT = BUILD; /** * Used for CLI binding. */ @CLIResolver public static AbstractProject resolveForCLI( @Argument(required=true,metaVar="NAME",usage="Job name") String name) throws CmdLineException { AbstractProject item = Hudson.getInstance().getItemByFullName(name, AbstractProject.class); if (item==null) throw new CmdLineException(null,Messages.AbstractItem_NoSuchJobExists(name,AbstractProject.findNearest(name).getFullName())); return item; } }