/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Martin Eigenbrodt, Matthew R. Harrah, Red Hat, Inc., Stephen Connolly, Tom Huybrechts, CloudBees, Inc.
*
* 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 com.google.common.base.Function;
import com.google.common.collect.Collections2;
import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
import hudson.Extension;
import hudson.ExtensionPoint;
import hudson.PermalinkList;
import hudson.Util;
import hudson.cli.declarative.CLIResolver;
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.ACL;
import hudson.tasks.LogRotator;
import hudson.util.AlternativeUiTextProvider;
import hudson.util.ChartUtil;
import hudson.util.ColorPalette;
import hudson.util.CopyOnWriteList;
import hudson.util.DataSetBuilder;
import hudson.util.DescribableList;
import hudson.util.FormApply;
import hudson.util.Graph;
import hudson.util.HttpResponses;
import hudson.util.IOException2;
import hudson.util.RunList;
import hudson.util.ShiftedCategoryAxis;
import hudson.util.StackedAreaRenderer2;
import hudson.util.TextFile;
import hudson.widgets.HistoryWidget;
import hudson.widgets.HistoryWidget.Adapter;
import hudson.widgets.Widget;
import jenkins.model.Jenkins;
import net.sf.json.JSONException;
import net.sf.json.JSONObject;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.axis.CategoryAxis;
import org.jfree.chart.axis.CategoryLabelPositions;
import org.jfree.chart.axis.NumberAxis;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.renderer.category.StackedAreaRenderer;
import org.jfree.data.category.CategoryDataset;
import org.jfree.ui.RectangleInsets;
import org.jvnet.localizer.Localizable;
import org.kohsuke.args4j.Argument;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.stapler.StaplerOverridable;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.export.Exported;
import javax.servlet.ServletException;
import java.awt.*;
import java.io.*;
import java.net.URLEncoder;
import java.util.*;
import java.util.List;
import static javax.servlet.http.HttpServletResponse.*;
/**
* A job is an runnable entity under the monitoring of Hudson.
*
*
* Every time it "runs", it will be recorded as a {@link Run} object.
*
*
* To create a custom job type, extend {@link TopLevelItemDescriptor} and put {@link Extension} on it.
*
* @author Kohsuke Kawaguchi
*/
public abstract class Job, RunT extends Run>
extends AbstractItem implements ExtensionPoint, StaplerOverridable {
/**
* 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.
*
* 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;
private volatile LogRotator logRotator;
/**
* 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 cachedBuildHealthReports = null;
private boolean keepDependencies;
/**
* List of {@link UserProperty}s configured for this project.
*/
// this should have been DescribableList but now it's too late
protected CopyOnWriteList> properties = new CopyOnWriteList>();
protected Job(ItemGroup parent, String name) {
super(parent, name);
}
@Override
public synchronized void save() throws IOException {
super.save();
holdOffBuildUntilSave = false;
}
@Override
public void onLoad(ItemGroup extends Item> parent, String name)
throws IOException {
super.onLoad(parent, name);
TextFile f = getNextBuildNumberFile();
if (f.exists()) {
// starting 1.28, we store nextBuildNumber in a separate file.
// but old Hudson didn't do it, so if the file doesn't exist,
// assume that nextBuildNumber was read from config.xml
try {
synchronized (this) {
this.nextBuildNumber = Integer.parseInt(f.readTrim());
}
} catch (NumberFormatException e) {
// try to infer the value of the next build number from the existing build records. See JENKINS-11563
File[] folders = this.getBuildDir().listFiles(new FileFilter() {
public boolean accept(File file) {
return file.isDirectory() && file.getName().matches("[0-9]+");
}
});
if (folders == null || folders.length == 0) {
this.nextBuildNumber = 1;
} else {
Collection foldersInt = Collections2.transform(Arrays.asList(folders), new Function() {
public Integer apply(File file) {
return Integer.parseInt(file.getName());
}
});
this.nextBuildNumber = Collections.max(foldersInt) + 1;
}
saveNextBuildNumber();
}
} else {
// From the old Hudson, or doCreateItem. Create this file now.
saveNextBuildNumber();
save(); // and delete it from the config.xml
}
if (properties == null) // didn't exist < 1.72
properties = new CopyOnWriteList>();
for (JobProperty p : properties)
p.setOwner(this);
}
@Override
public void onCopiedFrom(Item src) {
super.onCopiedFrom(src);
synchronized (this) {
this.nextBuildNumber = 1; // reset the next build number
this.holdOffBuildUntilSave = true;
}
}
@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?
}
}
super.performDelete();
}
/*package*/ TextFile getNextBuildNumberFile() {
return new TextFile(new File(this.getRootDir(), "nextBuildNumber"));
}
protected boolean isHoldOffBuildUntilSave() {
return holdOffBuildUntilSave;
}
protected synchronized void saveNextBuildNumber() throws IOException {
if (nextBuildNumber == 0) { // #3361
nextBuildNumber = 1;
}
getNextBuildNumberFile().write(String.valueOf(nextBuildNumber) + '\n');
}
@Exported
public boolean isInQueue() {
return false;
}
/**
* If this job is in the build queue, return its item.
*/
@Exported
public Queue.Item getQueueItem() {
return null;
}
/**
* Returns true if a build of this project is in progress.
*/
public boolean isBuilding() {
RunT b = getLastBuild();
return b!=null && b.isBuilding();
}
@Override
public String getPronoun() {
return AlternativeUiTextProvider.get(PRONOUN, this, Messages.Job_Pronoun());
}
/**
* Returns whether the name of this job can be changed by user.
*/
public boolean isNameEditable() {
return true;
}
/**
* If true, it will keep all the build logs of dependency components.
*/
@Exported
public boolean isKeepDependencies() {
return keepDependencies;
}
/**
* Allocates a new buildCommand number.
*/
public synchronized int assignBuildNumber() throws IOException {
int r = nextBuildNumber++;
saveNextBuildNumber();
return r;
}
/**
* Peeks the next build number.
*/
@Exported
public int getNextBuildNumber() {
return nextBuildNumber;
}
/**
* Programatically updates the next build number.
*
*
* Much of Hudson assumes that the build number is unique and monotonic, so
* this method can only accept a new value that's bigger than
* {@link #getLastBuild()} returns. Otherwise it'll be no-op.
*
* @since 1.199 (before that, this method was package private.)
*/
public synchronized void updateNextBuildNumber(int next) throws IOException {
RunT lb = getLastBuild();
if (lb!=null ? next>lb.getNumber() : next>0) {
this.nextBuildNumber = next;
saveNextBuildNumber();
}
}
/**
* Returns the log rotator for this job, or null if none.
*/
public LogRotator getLogRotator() {
return logRotator;
}
public void setLogRotator(LogRotator logRotator) {
this.logRotator = logRotator;
}
/**
* Perform log rotation.
*/
public void logRotate() throws IOException, InterruptedException {
LogRotator lr = getLogRotator();
if (lr != null)
lr.perform(this);
}
/**
* True if this instance supports log rotation configuration.
*/
public boolean supportsLogRotator() {
return true;
}
@Override
protected SearchIndexBuilder makeSearchIndex() {
return super.makeSearchIndex().add(new SearchIndex() {
public void find(String token, List 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 result) {
find(token, result);
}
}).add("configure", "config", "configure");
}
public Collection extends Job> getAllJobs() {
return Collections. singleton(this);
}
/**
* Adds {@link JobProperty}.
*
* @since 1.188
*/
public void addProperty(JobProperty super JobT> jobProp) throws IOException {
((JobProperty)jobProp).setOwner(this);
properties.add(jobProp);
save();
}
/**
* Removes {@link JobProperty}
*
* @since 1.279
*/
public void removeProperty(JobProperty super JobT> jobProp) throws IOException {
properties.remove(jobProp);
save();
}
/**
* Removes the property of the given type.
*
* @return
* The property that was just removed.
* @since 1.279
*/
public T removeProperty(Class clazz) throws IOException {
for (JobProperty super JobT> p : properties) {
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> getProperties() {
return Descriptor.toMap((Iterable) properties);
}
/**
* List of all {@link JobProperty} exposed primarily for the remoting API.
* @since 1.282
*/
@Exported(name="property",inline=true)
public List> getAllProperties() {
return properties.getView();
}
/**
* Gets the specific property, or null if the propert is not configured for
* this job.
*/
public T getProperty(Class clazz) {
for (JobProperty p : properties) {
if (clazz.isInstance(p))
return clazz.cast(p);
}
return null;
}
/**
* Bind {@link JobProperty}s to URL spaces.
*
* @since 1.403
*/
public JobProperty getProperty(String className) {
for (JobProperty p : properties)
if (p.getClass().getName().equals(className))
return p;
return null;
}
/**
* Overrides from job properties.
*/
public Collection> getOverrides() {
List