/******************************************************************************* * * Copyright (c) 2004-2013 Oracle Corporation. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * * Kohsuke Kawaguchi, Winston Prakash, Martin Eigenbrodt, Matthew R. Harrah, * Stephen Connolly, Tom Huybrechts, Anton Kozak, Nikita Levyankov, * Roy Varghese * *******************************************************************************/ package hudson.model; import com.google.common.collect.Sets; import hudson.Extension; import hudson.ExtensionPoint; import hudson.PermalinkList; import hudson.cli.declarative.CLIResolver; import hudson.model.BuildHistory.Record; import hudson.model.Descriptor.FormException; import hudson.model.Fingerprint.Range; import hudson.model.Fingerprint.RangeSet; import hudson.model.PermalinkProjectAction.Permalink; import hudson.search.QuickSilver; import hudson.search.SearchIndex; import hudson.search.SearchIndexBuilder; import hudson.search.SearchItem; import hudson.search.SearchItems; import hudson.security.*; import hudson.tasks.LogRotator; import hudson.util.BuildHistoryList; import hudson.util.CascadingUtil; import hudson.util.CopyOnWriteList; import hudson.util.DescribableList; import hudson.util.IOException2; import hudson.util.RunList; import hudson.util.TextFile; import hudson.widgets.HistoryWidget; import hudson.widgets.HistoryWidget.Adapter; import hudson.widgets.Widget; import java.awt.Color; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.ObjectStreamException; import java.io.PrintStream; import java.io.PrintWriter; import java.io.StringWriter; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.GregorianCalendar; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArraySet; import javax.servlet.ServletException; import static javax.servlet.http.HttpServletResponse.*; import net.sf.json.JSONException; import net.sf.json.JSONObject; import org.apache.commons.collections.ListUtils; import org.apache.commons.collections.MapUtils; import org.apache.commons.lang3.StringUtils; import org.eclipse.hudson.api.model.ICascadingJob; import org.eclipse.hudson.api.model.IJob; import org.eclipse.hudson.api.model.IProjectProperty; import org.eclipse.hudson.graph.*; import org.eclipse.hudson.model.project.property.BaseProjectProperty; import org.eclipse.hudson.model.project.property.CopyOnWriteListProjectProperty; import org.eclipse.hudson.model.project.property.DescribableListProjectProperty; import org.eclipse.hudson.model.project.property.ExternalProjectProperty; import org.eclipse.hudson.security.HudsonSecurityEntitiesHolder; import org.eclipse.hudson.security.team.Team; import org.eclipse.hudson.security.team.TeamManager; import org.jvnet.localizer.Localizable; import org.kohsuke.args4j.Argument; import org.kohsuke.args4j.CmdLineException; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.Stapler; import org.kohsuke.stapler.StaplerOverridable; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; import org.kohsuke.stapler.export.Exported; /** * A job is an runnable entity under the monitoring of Hudson. * * <p> Every time it "runs", it will be recorded as a {@link Run} object. * * <p> To create a custom job type, extend {@link TopLevelItemDescriptor} and * put {@link Extension} on it. * * @author Kohsuke Kawaguchi * @author Nikita Levyankov */ public abstract class Job<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, RunT>> extends AbstractItem implements ExtensionPoint, StaplerOverridable, IJob, ICascadingJob { private static transient final String HUDSON_BUILDS_PROPERTY_KEY = "HUDSON_BUILDS"; private static transient final String PROJECT_PROPERTY_KEY_PREFIX = "has"; private static transient final String BUILDS_DIRNAME = "builds"; public static final String PROPERTY_NAME_SEPARATOR = ";"; public static final String LOG_ROTATOR_PROPERTY_NAME = "logRotator"; public static final String PARAMETERS_DEFINITION_JOB_PROPERTY_PROPERTY_NAME = "parametersDefinitionProperties"; /** * Next build number. Kept in a separate file because this is the only * information that gets updated often. This allows the rest of the * configuration to be in the VCS. <p> In 1.28 and earlier, this field was * stored in the project configuration file, so even though this is marked * as transient, don't move it around. */ protected transient volatile int nextBuildNumber = 1; /** * Newly copied jobs get this flag set, so that Hudson doesn't try to run * the job until its configuration is saved once. */ private transient volatile boolean holdOffBuildUntilSave; /** * @deprecated as of 2.2.0 don't use this field directly, logic was moved to * {@link org.eclipse.hudson.api.model.IProjectProperty}. Use getter/setter * for accessing to this field. */ private volatile LogRotator logRotator; private transient ConcurrentMap<String, IProjectProperty> jobProperties = new ConcurrentHashMap<String, IProjectProperty>(); /** * This field is used for persisting only the overridenJobProperties in the config.xml */ private ConcurrentMap<String, IProjectProperty> persistableJobProperties = new ConcurrentHashMap<String, IProjectProperty>(); /** * Not all plugins are good at calculating their health report quickly. * These fields are used to cache the health reports to speed up rendering * the main page. */ private transient Integer cachedBuildHealthReportsBuildNumber = null; private transient List<HealthReport> cachedBuildHealthReports = null; private boolean keepDependencies; /** * The author of the job; */ protected volatile String createdBy; /** * The time when the job was created; */ private volatile long creationTime; /** * List of {@link UserProperty}s configured for this project. According to * new implementation {@link hudson.model.ParametersDefinitionProperty} were * moved from this collection. So, this field was left protected for * backward compatibility. Don't use this field directly for adding or * removing values. Use {@link #addProperty(JobProperty)}, {@link #removeProperty(JobProperty)}, * {@link #removeProperty(Class)} instead. * * @since 2.2.0 */ protected CopyOnWriteList<JobProperty<? super JobT>> properties = new CopyOnWriteList<JobProperty<? super JobT>>(); /** * The name of the cascadingProject. */ String cascadingProjectName; /** * The list with the names of children cascading projects. Required to avoid * cyclic references and to prohibition parent project "delete" action in * case it has cascading children projects. */ private Set<String> cascadingChildrenNames = new CopyOnWriteArraySet<String>(); /** * Set contains json-save names of cascadable {@link JobProperty} classes. * Intended to be used for cascading support of external hudson plugins, * that extends {@link JobProperty} class. See {@link #properties} field * description * * @since 2.2.0 */ private Set<String> cascadingJobProperties = new CopyOnWriteArraySet<String>(); /** * Selected cascadingProject for this job. */ protected transient JobT cascadingProject; private final static transient ThreadLocal<Boolean> allowSave = new ThreadLocal<Boolean>() { @Override protected Boolean initialValue() { return true; } }; protected Job(ItemGroup parent, String name) { super(parent, name); } public Object readResolve() { if (persistableJobProperties == null) { persistableJobProperties = new ConcurrentHashMap<String, IProjectProperty>(); } return this; } //Bug Fix: 406889 - Non overriden job properties or properties with no values should not be written to config.xml public Object writeReplace() throws ObjectStreamException, IOException { persistableJobProperties.clear(); for (String key : jobProperties.keySet()) { persistableJobProperties.put(key, jobProperties.get(key)); } // If the job has cascading parent then strip all the job properties that are not overriden // If the job has no cascading parent, then do not persist if the value is null or default value or has no elements // Caution: Do not check default value on CopyOnWriteListProjectProperty & DescribableListProjectProperty // it resets the value to empty list for (Iterator<Map.Entry<String, IProjectProperty>> it = persistableJobProperties.entrySet().iterator(); it.hasNext();) { Map.Entry<String, IProjectProperty> entry = it.next(); IProjectProperty projProperty = entry.getValue(); if (hasCascadingProject()) { if (!projProperty.isOverridden()) { it.remove(); } } else { if (projProperty instanceof CopyOnWriteListProjectProperty) { CopyOnWriteList list = (CopyOnWriteList) projProperty.getValue(); if (list.isEmpty()) { it.remove(); } } else if (projProperty instanceof DescribableListProjectProperty) { DescribableList list = (DescribableList) projProperty.getValue(); if (list.isEmpty()) { it.remove(); } } else if ((projProperty.getValue() == null) || (projProperty.getValue() == projProperty.getDefaultValue())) { it.remove(); } } } return this; } /** * Set true if save operation for config is permitted, false - otherwise . * * @param allowSave allow save. */ public void setAllowSave(Boolean allowSave) { this.allowSave.set(allowSave); } /** * Returns true if save operation for config is permitted. * * @return true if save operation for config is permitted. */ protected Boolean isAllowSave() { return allowSave.get(); } /** * {@inheritDoc} */ public void putProjectProperty(String key, IProjectProperty property) { if (null != key && null != property) { jobProperties.put(key, property); } } /** * {@inheritDoc} */ @SuppressWarnings({"unchecked"}) public Map<String, IProjectProperty> getProjectProperties() { return MapUtils.unmodifiableMap(jobProperties); } /** * {@inheritDoc} */ public void removeProjectProperty(String key) { jobProperties.remove(key); } /** * Put map of job properties to existing ones. * * @param projectProperties new properties map. * @param replace true - to replace current properties, false - add to * existing map */ protected void putAllProjectProperties(Map<String, ? extends IProjectProperty> projectProperties, boolean replace) { if (null != projectProperties) { if (replace) { jobProperties.clear(); } jobProperties.putAll(projectProperties); } } /** * {@inheritDoc} */ public IProjectProperty getProperty(String key) { return CascadingUtil.getProjectProperty(this, key); } /** * {@inheritDoc} */ public IProjectProperty getProperty(String key, Class clazz) { return CascadingUtil.getProjectProperty(this, key, clazz); } /** * {@inheritDoc} */ @Exported public Set<String> getCascadingChildrenNames() { return cascadingChildrenNames; } /** * {@inheritDoc} */ public void addCascadingChild(String cascadingChildName) throws IOException { cascadingChildrenNames.add(cascadingChildName); save(); } /** * {@inheritDoc} */ public void removeCascadingChild(String cascadingChildName) throws IOException { cascadingChildrenNames.remove(cascadingChildName); save(); } //Cleanup any cascading mess (called during Hudson startup) public void cleanCascading() throws IOException{ Set<String> cascadingChildrenToRemove = new HashSet(); for (String cascadingChild : getCascadingChildrenNames()) { TopLevelItem tlItem = Hudson.getInstance().getItem(cascadingChild); if ((tlItem != null) && getClass().isAssignableFrom(tlItem.getClass())) { JobT cascadingChildJob = (JobT) tlItem; if (cascadingChildJob.getCascadingProject() != this) { cascadingChildrenToRemove.add(cascadingChild); } }else{ cascadingChildrenToRemove.add(cascadingChild); } } //390862: Can't delete jobs copied from cascading parent if (!cascadingChildrenToRemove.isEmpty()) { cascadingChildrenNames.removeAll(cascadingChildrenToRemove); } //406889: Cleanup the non overridden job properties or properties with no values in config.xml save(); } /** * {@inheritDoc} */ public boolean hasCascadingChild(String cascadingChildName) { return null != cascadingChildName && cascadingChildrenNames.contains(cascadingChildName); } /** * {@inheritDoc} */ public synchronized void renameCascadingChildName(String oldChildName, String newChildName) throws IOException { cascadingChildrenNames.remove(oldChildName); cascadingChildrenNames.add(newChildName); save(); } @Override public synchronized void save() throws IOException { if (isAllowSave()) { super.save(); holdOffBuildUntilSave = false; } } private synchronized void setCascadingProject(){ if ((cascadingProjectName != null) && StringUtils.isNotBlank(cascadingProjectName)) { TopLevelItem tlItem = Hudson.getInstance().getItem(cascadingProjectName); //Fix: 413184. Gaurd against null, the job may be externally deleted or moved if ( (tlItem != null) && this.getClass().isAssignableFrom( tlItem.getClass())) { cascadingProject = (JobT) tlItem; } } } @Override @SuppressWarnings("unchecked") public void onLoad(ItemGroup<? extends Item> parent, String name) throws IOException { super.onLoad(parent, name); setCascadingProject(); TextFile f = getNextBuildNumberFile(); if (f.exists()) { try { synchronized (this) { this.nextBuildNumber = Integer.parseInt(f.readTrim()); } } catch (NumberFormatException e) { throw new IOException2(f + " doesn't contain a number", e); } } else { saveNextBuildNumber(); } if (properties == null) // didn't exist < 1.72 { properties = new CopyOnWriteList<JobProperty<? super JobT>>(); } if (cascadingChildrenNames == null) { cascadingChildrenNames = new CopyOnWriteArraySet<String>(); } jobProperties = new ConcurrentHashMap<String, IProjectProperty>(); for (String key : persistableJobProperties.keySet()) { jobProperties.put(key, persistableJobProperties.get(key)); } buildProjectProperties(); for (JobProperty p : getAllProperties()) { p.setOwner(this); } } /** * Resets overridden properties to the values defined in parent. * * @param propertyName the name of the properties. It possible to pass * several names separated with {@link #PROPERTY_NAME_SEPARATOR}. * @throws java.io.IOException exception. */ public void doResetProjectProperty(@QueryParameter final String propertyName) throws IOException { checkPermission(CONFIGURE); for (String name : StringUtils.split(propertyName, PROPERTY_NAME_SEPARATOR)) { final IProjectProperty property = getProperty(name); if (null != property) { property.resetValue(); } } save(); } protected void initAllowSave() { // No need to init, ThreadLocal is now a final static variable // allowSave = new ThreadLocal<Boolean>() { // @Override // protected Boolean initialValue() { // return true; // } // }; } /** * Initializes and builds project properties. Also converts legacy * properties to IProjectProperties. Subclasses should inherit and override * this behavior. * * @throws IOException if any. */ protected void buildProjectProperties() throws IOException { initProjectProperties(); for (Map.Entry<String, IProjectProperty> entry : jobProperties.entrySet()) { IProjectProperty property = entry.getValue(); property.setKey(entry.getKey()); property.setJob(this); } convertLogRotatorProperty(); convertJobProperties(); } void convertLogRotatorProperty() { if (null != logRotator && null == getProperty(LOG_ROTATOR_PROPERTY_NAME)) { setLogRotator(logRotator); logRotator = null; } } void convertJobProperties() { if (null != properties) { convertCascadingJobProperties(properties); } } /** * Adds cascading JobProperty. * * @param projectProperty BaseProjectProperty wrapper for JobProperty. */ private void addCascadingJobProperty(BaseProjectProperty projectProperty) { if (null != projectProperty) { cascadingJobProperties.add(projectProperty.getKey()); } } /** * Adds cascading JobProperty. * * @param cascadingJobPropertyKey key of cascading JobProperty. */ private void removeCascadingJobProperty(String cascadingJobPropertyKey) { if (null != cascadingJobPropertyKey) { IProjectProperty projectProperty = CascadingUtil.getProjectProperty(this, cascadingJobPropertyKey); if (null != projectProperty) { projectProperty.resetValue(); } cascadingJobProperties.remove(cascadingJobPropertyKey); } } /** * Initialize project properties if null. */ public final void initProjectProperties() { if (null == jobProperties) { jobProperties = new ConcurrentHashMap<String, IProjectProperty>(); } } @Override public void onCopiedFrom(Item src) { super.onCopiedFrom(src); synchronized (this) { this.nextBuildNumber = 1; // reset the next build number this.holdOffBuildUntilSave = true; this.creationTime = new GregorianCalendar().getTimeInMillis(); User user = User.current(); if (user != null) { this.createdBy = user.getId(); grantProjectMatrixPermissions(user); } } } /** * Grants project permissions to the user. * * @param user user */ protected void grantProjectMatrixPermissions(User user) { if (HudsonSecurityEntitiesHolder.getHudsonSecurityManager().getAuthorizationStrategy() instanceof ProjectMatrixAuthorizationStrategy) { Map<Permission, Set<String>> grantedPermissions = new HashMap<Permission, Set<String>>(); Set<String> users = Sets.newHashSet(user.getId()); grantedPermissions.put(Item.BUILD, users); grantedPermissions.put(Item.CONFIGURE, users); grantedPermissions.put(Item.DELETE, users); grantedPermissions.put(Item.READ, users); grantedPermissions.put(Item.WORKSPACE, users); grantedPermissions.put(Run.DELETE, users); grantedPermissions.put(Run.UPDATE, users); AuthorizationMatrixProperty amp = new AuthorizationMatrixProperty(grantedPermissions); amp.setOwner(this); properties.add(amp); } } @Override protected void performDelete() throws IOException, InterruptedException { // if a build is in progress. Cancel it. RunT lb = getLastBuild(); if (lb != null) { Executor e = lb.getExecutor(); if (e != null) { e.interrupt(); // should we block until the build is cancelled? } } Set<String> cascadingChildren = new HashSet(getCascadingChildrenNames()); for (String cascadingChild : cascadingChildren) { Item item = Hudson.getInstance().getItem(cascadingChild); if (item != null && item instanceof Job) { Job childJob = (Job) item; if (this.equals(childJob.getCascadingProject())) { childJob.clearCascadingProject(); } } } clearCascadingProject(); cascadingChildrenNames.clear(); super.performDelete(); } /*package*/ TextFile getNextBuildNumberFile() { File buildsDir = getBuildDir(); if (!buildsDir.exists()) { buildsDir.mkdirs(); } return new TextFile(new File(buildsDir, "../nextBuildNumber")); } protected boolean isHoldOffBuildUntilSave() { return holdOffBuildUntilSave; } protected synchronized void saveNextBuildNumber() throws IOException { if (nextBuildNumber == 0) { // #3361 nextBuildNumber = 1; } getNextBuildNumberFile().write(String.valueOf(nextBuildNumber) + '\n'); } /** * {@inheritDoc} */ @Exported public boolean isInQueue() { return false; } /** * {@inheritDoc} */ @Exported public Queue.Item getQueueItem() { return null; } /** * {@inheritDoc} */ public boolean isBuilding() { RunT b = getLastBuild(); return b != null && b.isBuilding(); } @Override public String getPronoun() { return Messages.Job_Pronoun(); } /** * {@inheritDoc} */ public boolean isNameEditable() { return true; } /** * If team enabled, allow user to edit only the unqualified portion * of the job name; otherwise, edit the full name. * @return editable name * @since 3.1.0 */ public String getEditableName() { String editableName = getName(); if (Hudson.getInstance().isTeamManagementEnabled()) { editableName = Hudson.getInstance().getTeamManager().getUnqualifiedJobName(editableName); } return editableName; } /** * {@inheritDoc} */ @Exported public boolean isKeepDependencies() { return keepDependencies; } /** * {@inheritDoc} */ public synchronized int assignBuildNumber() throws IOException { int r = nextBuildNumber++; saveNextBuildNumber(); return r; } /** * {@inheritDoc} */ @Exported public int getNextBuildNumber() { return nextBuildNumber; } /** * {@inheritDoc} */ public synchronized void updateNextBuildNumber(int next) throws IOException { RunT lb = getLastBuild(); if (lb != null ? next > lb.getNumber() : next > 0) { this.nextBuildNumber = next; saveNextBuildNumber(); } } /** * {@inheritDoc} */ public LogRotator getLogRotator() { return CascadingUtil.getLogRotatorProjectProperty(this, LOG_ROTATOR_PROPERTY_NAME).getValue(); } /** * {@inheritDoc} */ @SuppressWarnings("unchecked") public void setLogRotator(LogRotator logRotator) { CascadingUtil.getLogRotatorProjectProperty(this, LOG_ROTATOR_PROPERTY_NAME).setValue(logRotator); } /** * {@inheritDoc} */ public void logRotate() throws IOException, InterruptedException { LogRotator lr = getLogRotator(); if (lr != null) { lr.perform(this); } } /** * {@inheritDoc} */ public boolean supportsLogRotator() { return true; } /** * Method converts JobProperties to cascading values. * <p/> * If property is {@link AuthorizationMatrixProperty} - it will be skipped. * If property is {@link ParametersDefinitionProperty} - it will be added to * list of parameterDefinition properties. All the rest properties will be * converted to {@link BaseProjectProperty} classes and added to * cascadingJobProperties set. * * @param properties list of {@link JobProperty} */ @SuppressWarnings("unchecked") private void convertCascadingJobProperties(CopyOnWriteList<JobProperty<? super JobT>> properties) { CopyOnWriteList parameterDefinitionProperties = new CopyOnWriteList(); for (JobProperty property : properties) { if (property instanceof AuthorizationMatrixProperty) { continue; } if (property instanceof ParametersDefinitionProperty) { parameterDefinitionProperties.add(property); continue; } BaseProjectProperty projectProperty = CascadingUtil.getBaseProjectProperty(this, property.getDescriptor().getJsonSafeClassName()); addCascadingJobProperty(projectProperty); } if ((null == getProperty(PARAMETERS_DEFINITION_JOB_PROPERTY_PROPERTY_NAME)) && !parameterDefinitionProperties.isEmpty()) { setParameterDefinitionProperties(parameterDefinitionProperties); } } /** * @return list of cascading {@link JobProperty} instances. Includes * {@link ParametersDefinitionProperty} and children of {@link JobProperty} * from external plugins. */ @SuppressWarnings("unchecked") private CopyOnWriteList getCascadingJobProperties() { CopyOnWriteList result = new CopyOnWriteList(); CopyOnWriteList<ParametersDefinitionProperty> definitionProperties = getParameterDefinitionProperties(); if (null != cascadingJobProperties && !cascadingJobProperties.isEmpty()) { for (String key : cascadingJobProperties) { IProjectProperty projectProperty = CascadingUtil.getProjectProperty(this, key); if ((projectProperty != null) && (projectProperty.getValue() != null)) { result.add(projectProperty.getValue()); } } } if (null != definitionProperties && !definitionProperties.isEmpty()) { result.addAll(definitionProperties.getView()); } return result; } /** * Sets list of {@link ParametersDefinitionProperty}. Supports cascading * functionality. * * @param properties properties to set. */ private void setParameterDefinitionProperties(CopyOnWriteList<ParametersDefinitionProperty> properties) { CascadingUtil.setParameterDefinitionProperties(this, PARAMETERS_DEFINITION_JOB_PROPERTY_PROPERTY_NAME, properties); } /** * @return list of {@link ParametersDefinitionProperty}. Supports cascading * functionality. */ @SuppressWarnings("unchecked") private CopyOnWriteList<ParametersDefinitionProperty> getParameterDefinitionProperties() { return CascadingUtil.getCopyOnWriteListProjectProperty(this, PARAMETERS_DEFINITION_JOB_PROPERTY_PROPERTY_NAME) .getValue(); } @Override protected SearchIndexBuilder makeSearchIndex() { return super.makeSearchIndex().add(new SearchIndex() { public void find(String token, List<SearchItem> result) { try { if (token.startsWith("#")) { token = token.substring(1); // ignore leading '#' } int n = Integer.parseInt(token); Run b = getBuildByNumber(n); if (b == null) { return; // no such build } result.add(SearchItems.create("#" + n, "" + n, b)); } catch (NumberFormatException e) { // not a number. } } public void suggest(String token, List<SearchItem> result) { find(token, result); } }).add("configure", "config", "configure"); } public Collection<? extends Job> getAllJobs() { return Collections.<Job>singleton(this); } /** * Adds {@link JobProperty}. * * @since 1.188 */ @SuppressWarnings("unchecked") public void addProperty(JobProperty<? super JobT> jobProp) throws IOException { JobProperty jobProperty = (JobProperty) jobProp; jobProperty.setOwner(this); if (jobProperty instanceof AuthorizationMatrixProperty) { properties.add(jobProp); } else if (jobProperty instanceof ParametersDefinitionProperty) { CopyOnWriteList list = CascadingUtil.getCopyOnWriteListProjectProperty(this, PARAMETERS_DEFINITION_JOB_PROPERTY_PROPERTY_NAME).getOriginalValue(); if (null != list) { list.add(jobProp); } } else { BaseProjectProperty projectProperty = CascadingUtil.getBaseProjectProperty(this, jobProperty.getDescriptor().getJsonSafeClassName()); projectProperty.setValue(jobProperty); addCascadingJobProperty(projectProperty); } save(); } /** * Removes {@link JobProperty} * * @since 1.279 */ @SuppressWarnings("unchecked") public void removeProperty(JobProperty<? super JobT> jobProp) throws IOException { JobProperty jobProperty = (JobProperty) jobProp; if (jobProperty instanceof AuthorizationMatrixProperty) { properties.remove(jobProp); } else if (jobProperty instanceof ParametersDefinitionProperty) { CopyOnWriteList list = CascadingUtil.getCopyOnWriteListProjectProperty(this, PARAMETERS_DEFINITION_JOB_PROPERTY_PROPERTY_NAME).getOriginalValue(); if (null != list) { list.remove(jobProp); } } else { removeCascadingJobProperty(jobProperty.getDescriptor().getJsonSafeClassName()); } save(); } /** * Removes the property of the given type. * * @return The property that was just removed. * @since 1.279 */ @SuppressWarnings("unchecked") public <T extends JobProperty> T removeProperty(Class<T> clazz) throws IOException { CopyOnWriteList<JobProperty<? super JobT>> sourceProperties; if (clazz.equals(ParametersDefinitionProperty.class)) { sourceProperties = CascadingUtil.getCopyOnWriteListProjectProperty(this, PARAMETERS_DEFINITION_JOB_PROPERTY_PROPERTY_NAME).getOriginalValue(); } else if (clazz.equals(AuthorizationMatrixProperty.class)) { sourceProperties = properties; } else { sourceProperties = getCascadingJobProperties(); } if (null != sourceProperties) { for (JobProperty<? super JobT> p : sourceProperties) { if (clazz.isInstance(p)) { removeProperty(p); return clazz.cast(p); } } } return null; } /** * Gets all the job properties configured for this job. */ @SuppressWarnings("unchecked") public Map<JobPropertyDescriptor, JobProperty<? super JobT>> getProperties() { return Descriptor.toMap((Iterable) getAllProperties()); } /** * List of all {@link JobProperty} exposed primarily for the remoting API. * List contains cascadable {@link JobProperty} if any. * * @since 2.2.0 */ @Exported(name = "property", inline = true) @SuppressWarnings("unchecked") public List<JobProperty<? super JobT>> getAllProperties() { CopyOnWriteList cascadingJobProperties = getCascadingJobProperties(); List<JobProperty<? super JobT>> result = properties.getView(); if (null != cascadingJobProperties && !cascadingJobProperties.isEmpty()) { result = Collections.unmodifiableList(ListUtils.union(result, cascadingJobProperties.getView())); } return result; } /** * Gets the specific property, or null if the propert is not configured for * this job. Supports cascading properties * * @since 2.2.0 */ @SuppressWarnings("unchecked") public <T extends JobProperty> T getProperty(Class<T> clazz) { CopyOnWriteList<JobProperty<? super JobT>> sourceProperties; if (clazz.equals(AuthorizationMatrixProperty.class)) { sourceProperties = properties; } else { sourceProperties = getCascadingJobProperties(); } if (null != sourceProperties) { for (JobProperty p : sourceProperties) { if (clazz.isInstance(p)) { return clazz.cast(p); } } } return null; } /** * Overrides from job properties. */ public Collection<?> getOverrides() { List<Object> r = new ArrayList<Object>(); for (JobProperty<? super JobT> p : getAllProperties()) { r.addAll(p.getJobOverrides()); } return r; } public List<Widget> getWidgets() { ArrayList<Widget> r = new ArrayList<Widget>(); r.add(createHistoryWidget()); return r; } protected HistoryWidget createHistoryWidget() { return new HistoryWidget(this, getBuildHistoryData(), HISTORY_ADAPTER); } protected static final HistoryWidget.Adapter<BuildHistory.Record> HISTORY_ADAPTER = new Adapter<BuildHistory.Record>() { public int compare(BuildHistory.Record record, String key) { try { int k = Integer.parseInt(key); return record.getNumber() - k; } catch (NumberFormatException nfe) { return String.valueOf(record.getNumber()).compareTo(key); } } public String getKey(BuildHistory.Record record) { return String.valueOf(record.getNumber()); } public boolean isBuilding(BuildHistory.Record record) { return record.isBuilding(); } public String getNextKey(String key) { try { int k = Integer.parseInt(key); return String.valueOf(k + 1); } catch (NumberFormatException nfe) { return "-unable to determine next key-"; } } }; /** * @inheritDoc */ @Override protected void performBeforeItemRenaming(String oldName, String newName) throws IOException { CascadingUtil.renameCascadingChildLinks(cascadingProject, oldName, newName); CascadingUtil.renameCascadingParentLinks(oldName, newName); } /** * Renames a job. */ @Override public void renameTo(String newName) throws IOException { super.renameTo(newName); } /** * Returns true if we should display "build now" icon */ @Exported public abstract boolean isBuildable(); /** * Gets the read-only view of all the builds. * * @return never null. The first entry is the latest build. */ @Exported public RunList<RunT> getBuilds() { ByteArrayOutputStream os = new ByteArrayOutputStream(); PrintStream ps = new PrintStream(os); new Exception("Stack trace").printStackTrace(ps); LOGGER.debug("Job.getBuilds() API should be avoided for performance reason. {}", os.toString()); return RunList.fromRuns(_getRuns().values()); } /** * Obtains all the {@link Run}s whose build numbers matches the given * {@link RangeSet}. */ public synchronized List<RunT> getBuilds(RangeSet rs) { List<RunT> builds = new LinkedList<RunT>(); for (Range r : rs.getRanges()) { for (RunT b = getNearestBuild(r.start); b != null && b.getNumber() < r.end; b = b.getNextBuild()) { builds.add(b); } } return builds; } /** * Gets all the builds in a map. */ public SortedMap<Integer, RunT> getBuildsAsMap() { return Collections.unmodifiableSortedMap(_getRuns()); } /** * @deprecated since 2008-06-15. This is only used to support backward * compatibility with old URLs. */ @Deprecated public RunT getBuild(String id) { for (RunT r : _getRuns().values()) { if (r.getId().equals(id)) { return r; } } return null; } /** * @param n The build number. * @return null if no such build exists. * @see Run#getNumber() */ public RunT getBuildByNumber(int n) { return _getRuns().get(n); } /** * Obtains a list of builds, in the descending order, that are within the * specified time range [start,end). * * @return can be empty but never null. * @deprecated as of 1.372. Should just do * {@code getBuilds().byTimestamp(s,e)} to avoid code bloat in {@link Job}. */ public RunList<RunT> getBuildsByTimestamp(long start, long end) { return getBuilds().byTimestamp(start, end); } @CLIResolver public RunT getBuildForCLI(@Argument(required = true, metaVar = "BUILD#", usage = "Build number") String id) throws CmdLineException { try { int n = Integer.parseInt(id); RunT r = getBuildByNumber(n); if (r == null) { throw new CmdLineException(null, "No such build '#" + n + "' exists"); } return r; } catch (NumberFormatException e) { throw new CmdLineException(null, id + "is not a number"); } } /** * Gets the youngest build #m that satisfies <tt>n<=m</tt>. * * This is useful when you'd like to fetch a build but the exact build might * be already gone (deleted, rotated, etc.) */ public final RunT getNearestBuild(int n) { SortedMap<Integer, ? extends RunT> m = _getRuns().headMap(n - 1); // the map should // include n, so n-1 if (m.isEmpty()) { return null; } return m.get(m.lastKey()); } /** * Gets the latest build #m that satisfies <tt>m<=n</tt>. * * This is useful when you'd like to fetch a build but the exact build might * be already gone (deleted, rotated, etc.) */ public final RunT getNearestOldBuild(int n) { SortedMap<Integer, ? extends RunT> m = _getRuns().tailMap(n); if (m.isEmpty()) { return null; } return m.get(m.firstKey()); } @Override public Object getDynamic(String token, StaplerRequest req, StaplerResponse rsp) { try { // try to interpret the token as build number return _getRuns().get(Integer.valueOf(token)); } catch (NumberFormatException e) { // try to map that to widgets for (Widget w : getWidgets()) { if (w.getUrlName().equals(token)) { return w; } } // is this a permalink? for (Permalink p : getPermalinks()) { if (p.getId().equals(token)) { return p.resolve(this); } } return super.getDynamic(token, req, rsp); } } /** * Directory for storing {@link Run} records. * <p/> * Some {@link Job}s may not have backing data store for {@link Run}s, but * those {@link Job}s that use file system for storing data should use this * directory for consistency. This dir could be configured by setting * HUDSON_BUILDS property in JNDI or Environment or System properties. * * @return result directory * @see RunMap */ protected File getBuildDir() { String resultDir = getConfiguredHudsonProperty(HUDSON_BUILDS_PROPERTY_KEY); if (StringUtils.isNotBlank(resultDir)) { return new File(resultDir + "/" + getSearchName() + "/" + BUILDS_DIRNAME); } else { return new File(getRootDir(), BUILDS_DIRNAME); } } public abstract BuildHistory<JobT,RunT> getBuildHistoryData(); /** * Gets all the runs. * * The resulting map must be immutable (by employing copy-on-write * semantics.) The map is descending order, with newest builds at the top. */ protected abstract SortedMap<Integer, ? extends RunT> _getRuns(); /** * Called from {@link Run} to remove it from this job. * * The files are deleted already. So all the callee needs to do is to remove * a reference from this {@link Job}. */ protected abstract void removeRun(RunT run); /** * Returns the last build. */ @Exported @QuickSilver public RunT getLastBuild() { return getBuildHistoryData().getLastBuild(); } /** * Returns the oldest build in the record. */ @Exported @QuickSilver public RunT getFirstBuild() { return getBuildHistoryData().getFirstBuild(); } /** * Returns the last successful build, if any. Otherwise null. A successful * build would include either {@link Result#SUCCESS} or * {@link Result#UNSTABLE}. * * @see #getLastStableBuild() */ @Exported @QuickSilver public RunT getLastSuccessfulBuild() { return getBuildHistoryData().getLastSuccessfulBuild(); } /** * Returns the last build that was anything but stable, if any. Otherwise * null. * * @see #getLastSuccessfulBuild */ @Exported @QuickSilver public RunT getLastUnsuccessfulBuild() { return getBuildHistoryData().getLastUnsuccessfulBuild(); } /** * Returns the last unstable build, if any. Otherwise null. * * @see #getLastSuccessfulBuild */ @Exported @QuickSilver public RunT getLastUnstableBuild() { return getBuildHistoryData().getLastUnstableBuild(); } /** * Returns the last stable build, if any. Otherwise null. * * @see #getLastSuccessfulBuild */ @Exported @QuickSilver public RunT getLastStableBuild() { return getBuildHistoryData().getLastStableBuild(); } /** * Returns the last failed build, if any. Otherwise null. */ @Exported @QuickSilver public RunT getLastFailedBuild() { return getBuildHistoryData().getLastFailedBuild(); } /** * Returns the last completed build, if any. Otherwise null. */ @Exported @QuickSilver public RunT getLastCompletedBuild() { return getBuildHistoryData().getLastCompletedBuild(); } /** * Returns the last 'numberOfBuilds' builds with a build result >= * 'threshold' * * @return a list with the builds. May be smaller than 'numberOfBuilds' or * even empty if not enough builds satisfying the threshold have been found. * Never null. */ public List<RunT> getLastBuildsOverThreshold(int numberOfBuilds, Result threshold) { return getBuildHistoryData().getLastBuildsOverThreshold(numberOfBuilds, threshold); } public long getEstimatedDuration() { //List<RunT> builds = getLastBuildsOverThreshold(3, Result.UNSTABLE); List<Record<JobT, RunT>> records = getBuildHistoryData().getLastRecordsOverThreshold(3, Result.UNSTABLE); if (records.isEmpty()) { return -1; } long totalDuration = 0; for (Record b : records) { totalDuration += b.getDuration(); } if (totalDuration == 0) { return -1; } return Math.round((double) totalDuration / records.size()); } /** * Gets all the {@link Permalink}s defined for this job. * * @return never null */ public PermalinkList getPermalinks() { // TODO: shall we cache this? PermalinkList permalinks = new PermalinkList(Permalink.BUILTIN); for (Action a : getActions()) { if (a instanceof PermalinkProjectAction) { PermalinkProjectAction ppa = (PermalinkProjectAction) a; permalinks.addAll(ppa.getPermalinks()); } } return permalinks; } /** * Used as the color of the status ball for the project. */ @Exported(visibility = 2, name = "color") public BallColor getIconColor() { RunT lastBuild = getLastBuild(); while (lastBuild != null && lastBuild.hasntStartedYet()) { lastBuild = lastBuild.getPreviousBuild(); } if (lastBuild != null) { return lastBuild.getIconColor(); } else { return BallColor.GREY; } } /** * Get the current health report for a job. * * @return the health report. Never returns null */ public HealthReport getBuildHealth() { List<HealthReport> reports = getBuildHealthReports(); return reports.isEmpty() ? new HealthReport() : reports.get(0); } @Exported(name = "healthReport") public List<HealthReport> getBuildHealthReports() { List<HealthReport> reports = new ArrayList<HealthReport>(); RunT lastBuild = getLastBuild(); if (lastBuild != null && lastBuild.isBuilding()) { // show the previous build's report until the current one is // finished building. lastBuild = lastBuild.getPreviousBuild(); } // check the cache if (cachedBuildHealthReportsBuildNumber != null && cachedBuildHealthReports != null && lastBuild != null && cachedBuildHealthReportsBuildNumber.intValue() == lastBuild.getNumber()) { reports.addAll(cachedBuildHealthReports); } else if (lastBuild != null) { for (HealthReportingAction healthReportingAction : lastBuild.getActions(HealthReportingAction.class)) { final HealthReport report = healthReportingAction.getBuildHealth(); if (report != null) { if (report.isAggregateReport()) { reports.addAll(report.getAggregatedReports()); } else { reports.add(report); } } } final HealthReport report = getBuildStabilityHealthReport(); if (report != null) { if (report.isAggregateReport()) { reports.addAll(report.getAggregatedReports()); } else { reports.add(report); } } Collections.sort(reports); // store the cache cachedBuildHealthReportsBuildNumber = lastBuild.getNumber(); cachedBuildHealthReports = new ArrayList<HealthReport>(reports); } return reports; } private HealthReport getBuildStabilityHealthReport() { // we can give a simple view of build health from the last five builds int failCount = 0; int totalCount = 0; BuildHistory.Record r = getBuildHistoryData().getLast(); while (totalCount < 5 && r != null) { switch (r.getIconColor()) { case GREEN: case BLUE: case YELLOW: // failCount stays the same totalCount++; break; case RED: failCount++; totalCount++; break; default: // do nothing as these are inconclusive statuses break; } r = r.getPrevious(); } if (totalCount > 0) { int score = (int) ((100.0 * (totalCount - failCount)) / totalCount); Localizable description; if (failCount == 0) { description = Messages._Job_NoRecentBuildFailed(); } else if (totalCount == failCount) { // this should catch the case where totalCount == 1 // as failCount must be between 0 and totalCount // and we can't get here if failCount == 0 description = Messages._Job_AllRecentBuildFailed(); } else { description = Messages._Job_NOfMFailed(failCount, totalCount); } return new HealthReport(score, Messages._Job_BuildStability(description)); } return null; } // // // actions // // /** * Accepts submission from the configuration page. */ public synchronized void doConfigSubmit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, FormException { checkPermission(CONFIGURE); try { setAllowSave(false); submit(req, rsp); setAllowSave(true); save(); String newName = req.getParameter("name"); if (newName != null && !newName.equals(getEditableName())) { // check this error early to avoid HTTP response splitting. newName = Hudson.checkGoodJobName(newName); if (Hudson.getInstance().isTeamManagementEnabled()) { // Make the name qualified in the same team before confirm TeamManager teamManager = Hudson.getInstance().getTeamManager(); Team team = teamManager.findJobOwnerTeam(getName()); if (team != null) { // newName in this case is an unqualified job name, so fix newName = teamManager.getTeamQualifiedJobName(team, newName); } } rsp.sendRedirect("rename?newName=" + URLEncoder.encode(newName, "UTF-8")); } else { rsp.sendRedirect("."); } } catch (JSONException e) { StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw); pw.println("Failed to parse form data. Please report this problem as a bug"); try { pw.println("JSON=" + getSubmittedForm(req)); } catch (Exception ex) { pw.println("Unknown form exception while getting submitted form"); } pw.println(); e.printStackTrace(pw); rsp.setStatus(SC_BAD_REQUEST); sendError(sw.toString(), req, rsp, true); } } protected JSONObject getSubmittedForm(StaplerRequest request) throws FormException, ServletException { try { return request.getSubmittedForm(); } catch (IllegalStateException ex) { throw new FormException(ex.getMessage(), "unknown"); } } /** * Derived class can override this to perform additional config submission * work. */ protected void submit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, FormException { JSONObject json = getSubmittedForm(req); description = json.getString("description"); // Support both ways of setting dependencies // 1. directly in the root of the json // 2. within the fingerprinter section - for compatibility keepDependencies = false; if (json.has("keepDependencies")) { keepDependencies = json.getBoolean("keepDependencies"); } else { if ( json.has("hudson-tasks-Fingerprinter")) { JSONObject fingerPrinter = json.getJSONObject("hudson-tasks-Fingerprinter"); if ( fingerPrinter.has("keepDependencies")) { keepDependencies = fingerPrinter.getBoolean("keepDependencies"); } } } properties.clear(); setCascadingProjectName(StringUtils.trimToNull(json.getString("cascadingProjectName"))); CopyOnWriteList parameterDefinitionProperties = new CopyOnWriteList(); int i = 0; for (JobPropertyDescriptor d : JobPropertyDescriptor.getPropertyDescriptors(Job.this.getClass())) { if (!CascadingUtil.isCascadableJobProperty(d)) { String name = "jobProperty" + i; JSONObject config = json.getJSONObject(name); JobProperty prop = d.newInstance(req, config); if (null != prop) { prop.setOwner(this); if (prop instanceof AuthorizationMatrixProperty) { properties.add(prop); } else if (prop instanceof ParametersDefinitionProperty) { parameterDefinitionProperties.add(prop); } } } else { BaseProjectProperty property = CascadingUtil.getBaseProjectProperty(this, d.getJsonSafeClassName()); JobProperty prop = d.newInstance(req, json.getJSONObject(d.getJsonSafeClassName())); if (null != prop) { prop.setOwner(this); } property.setValue(prop); addCascadingJobProperty(property); } i++; } setParameterDefinitionProperties(parameterDefinitionProperties); LogRotator logRotator = null; if (json.has("logrotate")) { logRotator = LogRotator.DESCRIPTOR.newInstance(req, json.getJSONObject("logrotate")); } setLogRotator(logRotator); } /** * Accepts and serves the job description */ public void doDescription(StaplerRequest req, StaplerResponse rsp) throws IOException { if (req.getMethod().equals("GET")) { //read rsp.setContentType("text/plain;charset=UTF-8"); rsp.getWriter().write(this.getDescription()); return; } if (req.getMethod().equals("POST")) { checkPermission(CONFIGURE); // submission if (req.getParameter("description") != null) { this.setDescription(req.getParameter("description")); rsp.sendError(SC_NO_CONTENT); return; } } // huh? rsp.sendError(SC_BAD_REQUEST); } /** * Returns the image that shows the current buildCommand status. */ public void doBuildStatus(StaplerRequest req, StaplerResponse rsp) throws IOException { rsp.sendRedirect2(req.getContextPath() + "/images/48x48/" + getBuildStatusUrl()); } public String getBuildStatusUrl() { return getIconColor().getImage(); } /** * Renames this job. */ public/* not synchronized. see renameTo() */ void doDoRename( StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { requirePOST(); // rename is essentially delete followed by a create checkPermission(CREATE); checkPermission(DELETE); String newName = req.getParameter("newName"); String checkName = newName; if (isBuilding()) { // redirect to page explaining that we can't rename now rsp.sendRedirect("rename?newName=" + URLEncoder.encode(newName, "UTF-8")); return; } if (Hudson.getInstance().isTeamManagementEnabled()) { // Do this before rename or team will change to default for user TeamManager teamManager = Hudson.getInstance().getTeamManager(); Team team = teamManager.findJobOwnerTeam(getName()); if (team != null) { try { // newName must already be a qualified job name if (!teamManager.isQualifiedJobName(team, newName)) { throw new Failure("Job name "+newName+" is improperly qualified"); } checkName = teamManager.getUnqualifiedJobName(team, newName); teamManager.addJob(team, newName); } catch (TeamManager.TeamNotFoundException ex) { // Can't happen with non-null team } } } Hudson.checkGoodJobName(checkName); renameTo(newName); // send to the new job page // note we can't use getUrl() because that would pick up old name in the // Ancestor.getUrl() rsp.sendRedirect2(req.getContextPath() + '/' + getParent().getUrl() + getShortUrl()); } public void doRssAll(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { rss(req, rsp, " all builds", getBuilds()); } public void doRssFailed(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException { rss(req, rsp, " failed builds", getBuilds().failureOnly()); } private void rss(StaplerRequest req, StaplerResponse rsp, String suffix, RunList runs) throws IOException, ServletException { RSS.forwardToRss(getDisplayName() + suffix, getUrl(), runs.newBuilds(), Run.FEED_ADAPTER, req, rsp); } /** * Returns the {@link ACL} for this object. We need to override the * identical method in AbstractItem because we won't call getACL(Job) * otherwise (single dispatch) */ @Override public ACL getACL() { return HudsonSecurityEntitiesHolder.getHudsonSecurityManager().getAuthorizationStrategy().getACL(this); } public BuildTimelineWidget getTimeline() { final BuildHistoryList<JobT, RunT> bhl = BuildHistoryList.newBuildHistoryList(getBuildHistoryData()); return new BuildTimelineWidget(bhl); } /** * Returns the author of the job. * * @return the author of the job. * @since 2.0.1 */ public String getCreatedBy() { return createdBy; } /** * Sets the author of the job. * * @param createdBy the author of the job. */ protected void setCreatedBy(String createdBy) { this.createdBy = createdBy; } /** * Returns time when the project was created. * * @return time when the project was created. * @since 2.0.1 */ public long getCreationTime() { return creationTime; } /** * Sets time when the job was created. * * @param creationTime time when the job was created. */ protected void setCreationTime(long creationTime) { this.creationTime = creationTime; } /** * Returns cascading project name. * * @return cascading project name. */ public String getCascadingProjectName() { return cascadingProjectName; } public synchronized void doUpdateCascadingProject(@QueryParameter(fixEmpty = true) String projectName) throws IOException { setCascadingProjectName(projectName); } public synchronized void doModifyCascadingProperty(@QueryParameter(fixEmpty = true) String propertyName) { if (null != propertyName) { if (StringUtils.startsWith(propertyName, PROJECT_PROPERTY_KEY_PREFIX)) { propertyName = StringUtils.substring(propertyName, 3); propertyName = new StringBuilder(propertyName.length()) .append(Character.toLowerCase((propertyName.charAt(0)))) .append(propertyName.substring(1)) .toString(); } IProjectProperty property = getProperty(propertyName); if (null != property && property instanceof ExternalProjectProperty) { ((ExternalProjectProperty) property).setModified(true); } } } /** * Sets cascadingProject name and saves project configuration. * * @param cascadingProjectName cascadingProject name. * @throws java.io.IOException if configuration couldn't be saved. */ @SuppressWarnings("unchecked") @Override public synchronized void setCascadingProjectName(String cascadingProjectName) throws IOException { if (StringUtils.isBlank(cascadingProjectName)) { clearCascadingProject(); } else if (!StringUtils.equalsIgnoreCase(this.cascadingProjectName, cascadingProjectName)) { CascadingUtil.unlinkProjectFromCascadingParents(cascadingProject, name); this.cascadingProjectName = cascadingProjectName; TopLevelItem item = Hudson.getInstance().getItem(cascadingProjectName); if (item instanceof Job) { cascadingProject = (JobT) item; CascadingUtil.linkCascadingProjectsToChild(cascadingProject, name); for (IProjectProperty property : jobProperties.values()) { property.onCascadingProjectChanged(); } } } } /** * Renames cascading project name. For the properties processing and * children links updating please use {@link #setCascadingProjectName} * instead. * * @param cascadingProjectName new project name. * @throws java.io.IOException */ @Override public void renameCascadingProjectNameTo(String cascadingProjectName) throws IOException{ this.cascadingProjectName = cascadingProjectName; setCascadingProject(); save(); } /** * Returns selected cascading project. * * @return cascading project. */ @SuppressWarnings({"unchecked"}) @Override public JobT getCascadingProject() { // This is only for debugging. Do not set the instance to avoid thread contention, the cascadingProject must be set when // cascadingProjectname is set if ((cascadingProjectName != null) && StringUtils.isNotBlank(cascadingProjectName) && (cascadingProject == null)) { TopLevelItem item = Hudson.getInstance().getItem(cascadingProjectName); if (item instanceof Job) { LOGGER.error("Cascading job name set but the instance is not set. Job name: " + getName() + " Cascading parent name: " + cascadingProjectName); } } return cascadingProject; } /** * Checks whether current job is inherited from other project. * * @return boolean. */ public boolean hasCascadingProject() { return null != getCascadingProject(); } /** * Removes cascading project data, marks all project properties as * non-overridden and saves configuration * * @throws java.io.IOException if configuration couldn't be saved. */ private synchronized void clearCascadingProject() throws IOException { CascadingUtil.unlinkProjectFromCascadingParents(cascadingProject, name); this.cascadingProject = null; this.cascadingProjectName = null; for (IProjectProperty property : jobProperties.values()) { property.onCascadingProjectChanged(); } } public Graph getBuildTimeGraph() { Graph graph = new Graph(getLastBuild().getTimestamp(), 500, 400); DataSet<String, ChartLabel> data = new DataSet<String, ChartLabel>(); StaplerRequest req = Stapler.getCurrentRequest(); GraphSeries<String> xSeries = new GraphSeries<String>("Build No."); data.setXSeries(xSeries); GraphSeries<Number> ySeriesAborted = new GraphSeries<Number>(GraphSeries.TYPE_AREA, "Aborted", ColorPalette.GREY, false, false); ySeriesAborted.setBaseURL(getRelPath(req) + "/${buildNo}"); data.addYSeries(ySeriesAborted); GraphSeries<Number> ySeriesFailed = new GraphSeries<Number>(GraphSeries.TYPE_AREA, "Failed", ColorPalette.RED, false, false); ySeriesFailed.setBaseURL(getRelPath(req) + "/${buildNo}"); data.addYSeries(ySeriesFailed); GraphSeries<Number> ySeriesUnstable = new GraphSeries<Number>(GraphSeries.TYPE_AREA, "Unstable", ColorPalette.YELLOW, false, false); ySeriesUnstable.setBaseURL(getRelPath(req) + "/${buildNo}"); data.addYSeries(ySeriesUnstable); GraphSeries<Number> ySeriesSuccessful = new GraphSeries<Number>(GraphSeries.TYPE_AREA, "Successful", ColorPalette.BLUE, false, false); ySeriesSuccessful.setBaseURL(getRelPath(req) + "/${buildNo}"); data.addYSeries(ySeriesSuccessful); for (Run run : getBuilds()) { if (run.isBuilding()) { continue; } double duration = run.getDuration() / (1000 * 60); xSeries.add("#" + String.valueOf(run.number)); Result res = run.getResult(); if (res == Result.FAILURE) { ySeriesFailed.add(duration); ySeriesUnstable.add(0.); ySeriesAborted.add(0.); ySeriesSuccessful.add(0.); } else if (res == Result.UNSTABLE) { ySeriesUnstable.add(duration); ySeriesFailed.add(0.); ySeriesAborted.add(0.); ySeriesSuccessful.add(0.); } else if (res == Result.ABORTED || res == Result.NOT_BUILT) { ySeriesAborted.add(duration); ySeriesFailed.add(0.); ySeriesUnstable.add(0.); ySeriesSuccessful.add(0.); } else { ySeriesSuccessful.add(duration); ySeriesAborted.add(0.); ySeriesFailed.add(0.); ySeriesUnstable.add(0.); } // For backward compatibility with JFreechart data.add(duration, "min", new TimeTrendChartLabel(run)); } // We want to display the build result from older to latest data.reverseOrder(); graph.setYAxisLabel(Messages.Job_minutes()); graph.setData(data); return graph; } // For backward compatibility with JFreechart private class TimeTrendChartLabel extends ChartLabel { final Run run; public TimeTrendChartLabel(Run r) { this.run = r; } public int compareTo(ChartLabel that) { return Run.ORDER_BY_DATE.compare(run, ((TimeTrendChartLabel) that).run); } @Override public boolean equals(Object o) { // HUDSON-2682 workaround for Eclipse compilation bug // on (c instanceof ChartLabel) if (o == null || !ChartLabel.class.isAssignableFrom(o.getClass())) { return false; } TimeTrendChartLabel that = (TimeTrendChartLabel) o; return run == that.run; } public Color getColor(int row, int column) { // TODO: consider gradation. See // http://www.javadrive.jp/java2d/shape/index9.html Result r = run.getResult(); if (r == Result.FAILURE) { return ColorPalette.RED; } else if (r == Result.UNSTABLE) { return ColorPalette.YELLOW; } else if (r == Result.ABORTED || r == Result.NOT_BUILT) { return ColorPalette.GREY; } else { return ColorPalette.BLUE; } } @Override public int hashCode() { return run.hashCode(); } @Override public String toString() { String l = run.getDisplayName(); if (run instanceof Build) { String s = ((Build) run).getBuiltOnStr(); if (s != null) { l += ' ' + s; } } return l; } @Override public String getLink(int row, int column) { return String.valueOf(run.number); } @Override public String getToolTip(int row, int column) { return run.getDisplayName() + " : " + run.getDurationString(); } } private String getRelPath(StaplerRequest req) { String relPath = req.getParameter("rel"); if (relPath == null) { return ""; } return relPath; } }