File D:\maven-qcr-plugin-testsunitaires\qcr4java\jenkins-master\target\site\qc\core\src\main\java\hudson\model\Job.java.html

1 /*
2 * The MIT License
3 *
4 * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Martin Eigenbrodt, Matthew R. Harrah, Red Hat, Inc., Stephen Connolly, Tom Huybrechts, CloudBees, Inc.
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining a copy
7 * of this software and associated documentation files (the "Software"), to deal
8 * in the Software without restriction, including without limitation the rights
9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 * copies of the Software, and to permit persons to whom the Software is
11 * furnished to do so, subject to the following conditions:
12 *
13 * The above copyright notice and this permission notice shall be included in
14 * all copies or substantial portions of the Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 * THE SOFTWARE.
23 */
24 package hudson.model;
25
26 import com.google.common.base.Function;
27 import com.google.common.collect.Collections2;
28 import com.infradna.tool.bridge_method_injector.WithBridgeMethods;
29 import hudson.BulkChange;
30 import hudson.EnvVars;
31 import hudson.Extension;
32 import hudson.ExtensionPoint;
33 import hudson.PermalinkList;
34 import hudson.Util;
35 import hudson.cli.declarative.CLIResolver;
36 import hudson.model.Descriptor.FormException;
37 import hudson.model.Fingerprint.Range;
38 import hudson.model.Fingerprint.RangeSet;
39 import hudson.model.PermalinkProjectAction.Permalink;
40 import hudson.model.listeners.ItemListener;
41 import hudson.search.QuickSilver;
42 import hudson.search.SearchIndex;
43 import hudson.search.SearchIndexBuilder;
44 import hudson.search.SearchItem;
45 import hudson.search.SearchItems;
46 import hudson.security.ACL;
47 import hudson.tasks.LogRotator;
48 import hudson.util.AlternativeUiTextProvider;
49 import hudson.util.ChartUtil;
50 import hudson.util.ColorPalette;
51 import hudson.util.CopyOnWriteList;
52 import hudson.util.DataSetBuilder;
53 import hudson.util.DescribableList;
54 import hudson.util.FormApply;
55 import hudson.util.Graph;
56 import hudson.util.ProcessTree;
57 import hudson.util.RunList;
58 import hudson.util.ShiftedCategoryAxis;
59 import hudson.util.StackedAreaRenderer2;
60 import hudson.util.TextFile;
61 import hudson.widgets.HistoryWidget;
62 import hudson.widgets.HistoryWidget.Adapter;
63 import hudson.widgets.Widget;
64 import jenkins.model.BuildDiscarder;
65 import jenkins.model.Jenkins;
66 import jenkins.model.ProjectNamingStrategy;
67 import jenkins.scm.SCMCheckoutStrategy;
68 import jenkins.security.HexStringConfidentialKey;
69 import jenkins.util.io.OnMaster;
70 import net.sf.json.JSONException;
71 import net.sf.json.JSONObject;
72 import org.jfree.chart.ChartFactory;
73 import org.jfree.chart.JFreeChart;
74 import org.jfree.chart.axis.CategoryAxis;
75 import org.jfree.chart.axis.CategoryLabelPositions;
76 import org.jfree.chart.axis.NumberAxis;
77 import org.jfree.chart.plot.CategoryPlot;
78 import org.jfree.chart.plot.PlotOrientation;
79 import org.jfree.chart.renderer.category.StackedAreaRenderer;
80 import org.jfree.data.category.CategoryDataset;
81 import org.jfree.ui.RectangleInsets;
82 import org.jvnet.localizer.Localizable;
83 import org.kohsuke.args4j.Argument;
84 import org.kohsuke.args4j.CmdLineException;
85 import org.kohsuke.stapler.StaplerOverridable;
86 import org.kohsuke.stapler.StaplerRequest;
87 import org.kohsuke.stapler.StaplerResponse;
88 import org.kohsuke.stapler.export.Exported;
89 import org.kohsuke.stapler.interceptor.RequirePOST;
90
91 import javax.servlet.ServletException;
92 import java.awt.*;
93 import java.io.*;
94 import java.net.URLEncoder;
95 import java.util.*;
96 import java.util.List;
97
98 import static javax.servlet.http.HttpServletResponse.*;
99
100 /**
101 * A job is an runnable entity under the monitoring of Hudson.
102 *
103 * <p>
104 * Every time it "runs", it will be recorded as a {@link Run} object.
105 *
106 * <p>
107 * To create a custom job type, extend {@link TopLevelItemDescriptor} and put {@link Extension} on it.
108 *
109 * @author Kohsuke Kawaguchi
110 */
111 public abstract class Job<JobT extends Job<JobT, RunT>, RunT extends Run<JobT, RunT>>
112 extends AbstractItem implements ExtensionPoint, StaplerOverridable, OnMaster {
113
114 /**
115 * Next build number. Kept in a separate file because this is the only
116 * information that gets updated often. This allows the rest of the
117 * configuration to be in the VCS.
118 * <p>
119 * In 1.28 and earlier, this field was stored in the project configuration
120 * file, so even though this is marked as transient, don't move it around.
121 */
122 protected transient volatile int nextBuildNumber = 1;
123
124 /**
125 * Newly copied jobs get this flag set, so that Hudson doesn't try to run the job until its configuration
126 * is saved once.
127 */
128 private transient volatile boolean holdOffBuildUntilSave;
129
130 private volatile BuildDiscarder logRotator;
131
132 /**
133 * Not all plugins are good at calculating their health report quickly.
134 * These fields are used to cache the health reports to speed up rendering
135 * the main page.
136 */
137 private transient Integer cachedBuildHealthReportsBuildNumber = null;
138 private transient List<HealthReport> cachedBuildHealthReports = null;
139
140 private boolean keepDependencies;
141
142 /**
143 * List of {@link UserProperty}s configured for this project.
144 */
145 // this should have been DescribableList but now it's too late
146 protected CopyOnWriteList<JobProperty<? super JobT>> properties = new CopyOnWriteList<JobProperty<? super JobT>>();
147
148 protected Job(ItemGroup parent, String name) {
149 super(parent, name);
150 }
151
152 @Override
153 public synchronized void save() throws IOException {
154 super.save();
155 holdOffBuildUntilSave = false;
156 }
157
158 @Override
159 public void onLoad(ItemGroup<? extends Item> parent, String name)
160 throws IOException {
161 super.onLoad(parent, name);
162
163 TextFile f = getNextBuildNumberFile();
164 if (f.exists()) {
165 // starting 1.28, we store nextBuildNumber in a separate file.
166 // but old Hudson didn't do it, so if the file doesn't exist,
167 // assume that nextBuildNumber was read from config.xml
168 try {
169 synchronized (this) {
170 this.nextBuildNumber = Integer.parseInt(f.readTrim());
171 }
172 } catch (NumberFormatException e) {
173 // try to infer the value of the next build number from the existing build records. See JENKINS-11563
174 File[] folders = this.getBuildDir().listFiles(new FileFilter() {
175 public boolean accept(File file) {
176 return file.isDirectory() && file.getName().matches("[0-9]+");
177 }
178 });
179
180 if (folders == null || folders.length == 0) {
181 this.nextBuildNumber = 1;
182 } else {
183 Collection<Integer> foldersInt = Collections2.transform(Arrays.asList(folders), new Function<File, Integer>() {
184 public Integer apply(File file) {
185 return Integer.parseInt(file.getName());
186 }
187 });
188 this.nextBuildNumber = Collections.max(foldersInt) + 1;
189 }
190 saveNextBuildNumber();
191 }
192 } else {
193 // From the old Hudson, or doCreateItem. Create this file now.
194 saveNextBuildNumber();
195 }
196
197 if (properties == null) // didn't exist < 1.72
198 properties = new CopyOnWriteList<JobProperty<? super JobT>>();
199
200 for (JobProperty p : properties)
201 p.setOwner(this);
202 }
203
204 @Override
205 public void onCopiedFrom(Item src) {
206 super.onCopiedFrom(src);
207 synchronized (this) {
208 this.nextBuildNumber = 1; // reset the next build number
209 this.holdOffBuildUntilSave = true;
210 }
211 }
212
213 @Override
214 protected void performDelete() throws IOException, InterruptedException {
215 // if a build is in progress. Cancel it.
216 RunT lb = getLastBuild();
217 if (lb != null) {
218 Executor e = lb.getExecutor();
219 if (e != null) {
220 e.interrupt();
221 // should we block until the build is cancelled?
222 }
223 }
224 super.performDelete();
225 }
226
227 /*package*/ TextFile getNextBuildNumberFile() {
228 return new TextFile(new File(this.getRootDir(), "nextBuildNumber"));
229 }
230
231 protected boolean isHoldOffBuildUntilSave() {
232 return holdOffBuildUntilSave;
233 }
234
235 protected synchronized void saveNextBuildNumber() throws IOException {
236 if (nextBuildNumber == 0) { // #3361
237 nextBuildNumber = 1;
238 }
239 getNextBuildNumberFile().write(String.valueOf(nextBuildNumber) + '\n');
240 }
241
242 @Exported
243 public boolean isInQueue() {
244 return false;
245 }
246
247 /**
248 * If this job is in the build queue, return its item.
249 */
250 @Exported
251 public Queue.Item getQueueItem() {
252 return null;
253 }
254
255 /**
256 * Returns true if a build of this project is in progress.
257 */
258 public boolean isBuilding() {
259 RunT b = getLastBuild();
260 return b!=null && b.isBuilding();
261 }
262
263 /**
264 * Returns true if the log file is still being updated.
265 */
266 public boolean isLogUpdated() {
267 RunT b = getLastBuild();
268 return b!=null && b.isLogUpdated();
269 }
270
271 @Override
272 public String getPronoun() {
273 return AlternativeUiTextProvider.get(PRONOUN, this, Messages.Job_Pronoun());
274 }
275
276 /**
277 * Returns whether the name of this job can be changed by user.
278 */
279 public boolean isNameEditable() {
280 return true;
281 }
282
283 /**
284 * If true, it will keep all the build logs of dependency components.
285 */
286 @Exported
287 public boolean isKeepDependencies() {
288 return keepDependencies;
289 }
290
291 /**
292 * Allocates a new buildCommand number.
293 */
294 public synchronized int assignBuildNumber() throws IOException {
295 int r = nextBuildNumber++;
296 saveNextBuildNumber();
297 return r;
298 }
299
300 /**
301 * Peeks the next build number.
302 */
303 @Exported
304 public int getNextBuildNumber() {
305 return nextBuildNumber;
306 }
307
308 /**
309 * Builds up the environment variable map that's sufficient to identify a process
310 * as ours. This is used to kill run-away processes via {@link ProcessTree#killAll(Map)}.
311 */
312 public EnvVars getCharacteristicEnvVars() {
313 EnvVars env = new EnvVars();
314 env.put("JENKINS_SERVER_COOKIE",SERVER_COOKIE.get());
315 env.put("HUDSON_SERVER_COOKIE",SERVER_COOKIE.get()); // Legacy compatibility
316 env.put("JOB_NAME",getFullName());
317 return env;
318 }
319
320 /**
321 * Creates an environment variable override for launching processes for this project.
322 *
323 * <p>
324 * This is for process launching outside the build execution (such as polling, tagging, deployment, etc.)
325 * that happens in a context of a specific job.
326 *
327 * @param node
328 * Node to eventually run a process on. The implementation must cope with this parameter being null
329 * (in which case none of the node specific properties would be reflected in the resulting override.)
330 */
331 public EnvVars getEnvironment(Node node, TaskListener listener) throws IOException, InterruptedException {
332 EnvVars env;
333
334 if (node!=null)
335 env = node.toComputer().buildEnvironment(listener);
336 else
337 env = new EnvVars();
338
339 env.putAll(getCharacteristicEnvVars());
340
341 // servlet container may have set CLASSPATH in its launch script,
342 // so don't let that inherit to the new child process.
343 // see http://www.nabble.com/Run-Job-with-JDK-1.4.2-tf4468601.html
344 env.put("CLASSPATH","");
345
346 return env;
347 }
348
349 /**
350 * Programatically updates the next build number.
351 *
352 * <p>
353 * Much of Hudson assumes that the build number is unique and monotonic, so
354 * this method can only accept a new value that's bigger than
355 * {@link #getLastBuild()} returns. Otherwise it'll be no-op.
356 *
357 * @since 1.199 (before that, this method was package private.)
358 */
359 public synchronized void updateNextBuildNumber(int next) throws IOException {
360 RunT lb = getLastBuild();
361 if (lb!=null ? next>lb.getNumber() : next>0) {
362 this.nextBuildNumber = next;
363 saveNextBuildNumber();
364 }
365 }
366
367 /**
368 * Returns the configured build discarder for this job, or null if none.
369 */
370 public BuildDiscarder getBuildDiscarder() {
371 return logRotator;
372 }
373
374 public void setBuildDiscarder(BuildDiscarder bd) throws IOException {
375 this.logRotator = bd;
376 save();
377 }
378
379 /**
380 * Left for backward compatibility. Returns non-null if and only
381 * if {@link LogRotator} is configured as {@link BuildDiscarder}.
382 *
383 * @deprecated as of 1.503
384 * Use {@link #getBuildDiscarder()}.
385 */
386 public LogRotator getLogRotator() {
387 if (logRotator instanceof LogRotator)
388 return (LogRotator) logRotator;
389 return null;
390 }
391
392 /**
393 * @deprecated as of 1.503
394 * Use {@link #setBuildDiscarder(BuildDiscarder)}
395 */
396 public void setLogRotator(LogRotator logRotator) throws IOException {
397 setBuildDiscarder(logRotator);
398 }
399
400 /**
401 * Perform log rotation.
402 */
403 public void logRotate() throws IOException, InterruptedException {
404 BuildDiscarder bd = getBuildDiscarder();
405 if (bd != null)
406 bd.perform(this);
407 }
408
409 /**
410 * True if this instance supports log rotation configuration.
411 */
412 public boolean supportsLogRotator() {
413 return true;
414 }
415
416 @Override
417 protected SearchIndexBuilder makeSearchIndex() {
418 return super.makeSearchIndex().add(new SearchIndex() {
419 public void find(String token, List<SearchItem> result) {
420 try {
421 if (token.startsWith("#"))
422 token = token.substring(1); // ignore leading '#'
423 int n = Integer.parseInt(token);
424 Run b = getBuildByNumber(n);
425 if (b == null)
426 return; // no such build
427 result.add(SearchItems.create("#" + n, "" + n, b));
428 } catch (NumberFormatException e) {
429 // not a number.
430 }
431 }
432
433 public void suggest(String token, List<SearchItem> result) {
434 find(token, result);
435 }
436 }).add("configure", "config", "configure");
437 }
438
439 public Collection<? extends Job> getAllJobs() {
440 return Collections.<Job> singleton(this);
441 }
442
443 /**
444 * Adds {@link JobProperty}.
445 *
446 * @since 1.188
447 */
448 public void addProperty(JobProperty<? super JobT> jobProp) throws IOException {
449 ((JobProperty)jobProp).setOwner(this);
450 properties.add(jobProp);
451 save();
452 }
453
454 /**
455 * Removes {@link JobProperty}
456 *
457 * @since 1.279
458 */
459 public void removeProperty(JobProperty<? super JobT> jobProp) throws IOException {
460 properties.remove(jobProp);
461 save();
462 }
463
464 /**
465 * Removes the property of the given type.
466 *
467 * @return
468 * The property that was just removed.
469 * @since 1.279
470 */
471 public <T extends JobProperty> T removeProperty(Class<T> clazz) throws IOException {
472 for (JobProperty<? super JobT> p : properties) {
473 if (clazz.isInstance(p)) {
474 removeProperty(p);
475 return clazz.cast(p);
476 }
477 }
478 return null;
479 }
480
481 /**
482 * Gets all the job properties configured for this job.
483 */
484 @SuppressWarnings("unchecked")
485 public Map<JobPropertyDescriptor, JobProperty<? super JobT>> getProperties() {
486 return Descriptor.toMap((Iterable) properties);
487 }
488
489 /**
490 * List of all {@link JobProperty} exposed primarily for the remoting API.
491 * @since 1.282
492 */
493 @Exported(name="property",inline=true)
494 public List<JobProperty<? super JobT>> getAllProperties() {
495 return properties.getView();
496 }
497
498 /**
499 * Gets the specific property, or null if the propert is not configured for
500 * this job.
501 */
502 public <T extends JobProperty> T getProperty(Class<T> clazz) {
503 for (JobProperty p : properties) {
504 if (clazz.isInstance(p))
505 return clazz.cast(p);
506 }
507 return null;
508 }
509
510 /**
511 * Bind {@link JobProperty}s to URL spaces.
512 *
513 * @since 1.403
514 */
515 public JobProperty getProperty(String className) {
516 for (JobProperty p : properties)
517 if (p.getClass().getName().equals(className))
518 return p;
519 return null;
520 }
521
522 /**
523 * Overrides from job properties.
524 * @see JobProperty#getJobOverrides
525 */
526 public Collection<?> getOverrides() {
527 List<Object> r = new ArrayList<Object>();
528 for (JobProperty<? super JobT> p : properties)
529 r.addAll(p.getJobOverrides());
530 return r;
531 }
532
533 public List<Widget> getWidgets() {
534 ArrayList<Widget> r = new ArrayList<Widget>();
535 r.add(createHistoryWidget());
536 return r;
537 }
538
539 protected HistoryWidget createHistoryWidget() {
540 return new HistoryWidget<Job, RunT>(this, getBuilds(), HISTORY_ADAPTER);
541 }
542
543 protected static final HistoryWidget.Adapter<Run> HISTORY_ADAPTER = new Adapter<Run>() {
544 public int compare(Run record, String key) {
545 try {
546 int k = Integer.parseInt(key);
547 return record.getNumber() - k;
548 } catch (NumberFormatException nfe) {
549 return String.valueOf(record.getNumber()).compareTo(key);
550 }
551 }
552
553 public String getKey(Run record) {
554 return String.valueOf(record.getNumber());
555 }
556
557 public boolean isBuilding(Run record) {
558 return record.isBuilding();
559 }
560
561 public String getNextKey(String key) {
562 try {
563 int k = Integer.parseInt(key);
564 return String.valueOf(k + 1);
565 } catch (NumberFormatException nfe) {
566 return "-unable to determine next key-";
567 }
568 }
569 };
570
571 /**
572 * Renames a job.
573 */
574 @Override
575 public void renameTo(String newName) throws IOException {
576 super.renameTo(newName);
577 }
578
579 /**
580 * Returns true if we should display "build now" icon
581 */
582 @Exported
583 public abstract boolean isBuildable();
584
585 /**
586 * Gets the read-only view of all the builds.
587 *
588 * @return never null. The first entry is the latest build.
589 */
590 @Exported(name="allBuilds",visibility=-2)
591 @WithBridgeMethods(List.class)
592 public RunList<RunT> getBuilds() {
593 return RunList.fromRuns(_getRuns().values());
594 }
595
596 /**
597 * Gets the read-only view of the recent builds.
598 *
599 * @since 1.485
600 */
601 @Exported(name="builds")
602 public RunList<RunT> getNewBuilds() {
603 return getBuilds().newBuilds();
604 }
605
606 /**
607 * Obtains all the {@link Run}s whose build numbers matches the given {@link RangeSet}.
608 */
609 public synchronized List<RunT> getBuilds(RangeSet rs) {
610 List<RunT> builds = new LinkedList<RunT>();
611
612 for (Range r : rs.getRanges()) {
613 for (RunT b = getNearestBuild(r.start); b!=null && b.getNumber()<r.end; b=b.getNextBuild()) {
614 builds.add(b);
615 }
616 }
617
618 return builds;
619 }
620
621 /**
622 * Gets all the builds in a map.
623 */
624 public SortedMap<Integer, RunT> getBuildsAsMap() {
625 return Collections.unmodifiableSortedMap(_getRuns());
626 }
627
628 /**
629 * @deprecated since 2008-06-15.
630 * This is only used to support backward compatibility with old URLs.
631 */
632 @Deprecated
633 public RunT getBuild(String id) {
634 for (RunT r : _getRuns().values()) {
635 if (r.getId().equals(id))
636 return r;
637 }
638 return null;
639 }
640
641 /**
642 * @param n
643 * The build number.
644 * @return null if no such build exists.
645 * @see Run#getNumber()
646 */
647 public RunT getBuildByNumber(int n) {
648 return _getRuns().get(n);
649 }
650
651 /**
652 * Obtains a list of builds, in the descending order, that are within the specified time range [start,end).
653 *
654 * @return can be empty but never null.
655 * @deprecated
656 * as of 1.372. Should just do {@code getBuilds().byTimestamp(s,e)} to avoid code bloat in {@link Job}.
657 */
658 @WithBridgeMethods(List.class)
659 public RunList<RunT> getBuildsByTimestamp(long start, long end) {
660 return getBuilds().byTimestamp(start,end);
661 }
662
663 @CLIResolver
664 public RunT getBuildForCLI(@Argument(required=true,metaVar="BUILD#",usage="Build number") String id) throws CmdLineException {
665 try {
666 int n = Integer.parseInt(id);
667 RunT r = getBuildByNumber(n);
668 if (r==null)
669 throw new CmdLineException(null, "No such build '#"+n+"' exists");
670 return r;
671 } catch (NumberFormatException e) {
672 throw new CmdLineException(null, id+ "is not a number");
673 }
674 }
675
676 /**
677 * Gets the youngest build #m that satisfies <tt>n&lt;=m</tt>.
678 *
679 * This is useful when you'd like to fetch a build but the exact build might
680 * be already gone (deleted, rotated, etc.)
681 */
682 public RunT getNearestBuild(int n) {
683 SortedMap<Integer, ? extends RunT> m = _getRuns().headMap(n - 1); // the map should
684 // include n, so n-1
685 if (m.isEmpty())
686 return null;
687 return m.get(m.lastKey());
688 }
689
690 /**
691 * Gets the latest build #m that satisfies <tt>m&lt;=n</tt>.
692 *
693 * This is useful when you'd like to fetch a build but the exact build might
694 * be already gone (deleted, rotated, etc.)
695 */
696 public RunT getNearestOldBuild(int n) {
697 SortedMap<Integer, ? extends RunT> m = _getRuns().tailMap(n);
698 if (m.isEmpty())
699 return null;
700 return m.get(m.firstKey());
701 }
702
703 @Override
704 public Object getDynamic(String token, StaplerRequest req,
705 StaplerResponse rsp) {
706 try {
707 // try to interpret the token as build number
708 return getBuildByNumber(Integer.valueOf(token));
709 } catch (NumberFormatException e) {
710 // try to map that to widgets
711 for (Widget w : getWidgets()) {
712 if (w.getUrlName().equals(token))
713 return w;
714 }
715
716 // is this a permalink?
717 for (Permalink p : getPermalinks()) {
718 if(p.getId().equals(token))
719 return p.resolve(this);
720 }
721
722 return super.getDynamic(token, req, rsp);
723 }
724 }
725
726 /**
727 * Directory for storing {@link Run} records.
728 * <p>
729 * Some {@link Job}s may not have backing data store for {@link Run}s, but
730 * those {@link Job}s that use file system for storing data should use this
731 * directory for consistency.
732 *
733 * @see RunMap
734 */
735 protected File getBuildDir() {
736 return Jenkins.getInstance().getBuildDirFor(this);
737 }
738
739 /**
740 * Gets all the runs.
741 *
742 * The resulting map must be treated immutable (by employing copy-on-write
743 * semantics.) The map is descending order, with newest builds at the top.
744 */
745 protected abstract SortedMap<Integer, ? extends RunT> _getRuns();
746
747 /**
748 * Called from {@link Run} to remove it from this job.
749 *
750 * The files are deleted already. So all the callee needs to do is to remove
751 * a reference from this {@link Job}.
752 */
753 protected abstract void removeRun(RunT run);
754
755 /**
756 * Returns the last build.
757 */
758 @Exported
759 @QuickSilver
760 public RunT getLastBuild() {
761 SortedMap<Integer, ? extends RunT> runs = _getRuns();
762
763 if (runs.isEmpty())
764 return null;
765 return runs.get(runs.firstKey());
766 }
767
768 /**
769 * Returns the oldest build in the record.
770 */
771 @Exported
772 @QuickSilver
773 public RunT getFirstBuild() {
774 SortedMap<Integer, ? extends RunT> runs = _getRuns();
775
776 if (runs.isEmpty())
777 return null;
778 return runs.get(runs.lastKey());
779 }
780
781 /**
782 * Returns the last successful build, if any. Otherwise null. A successful build
783 * would include either {@link Result#SUCCESS} or {@link Result#UNSTABLE}.
784 *
785 * @see #getLastStableBuild()
786 */
787 @Exported
788 @QuickSilver
789 public RunT getLastSuccessfulBuild() {
790 RunT r = getLastBuild();
791 // temporary hack till we figure out what's causing this bug
792 while (r != null
793 && (r.isBuilding() || r.getResult() == null || r.getResult()
794 .isWorseThan(Result.UNSTABLE)))
795 r = r.getPreviousBuild();
796 return r;
797 }
798
799 /**
800 * Returns the last build that was anything but stable, if any. Otherwise null.
801 * @see #getLastSuccessfulBuild
802 */
803 @Exported
804 @QuickSilver
805 public RunT getLastUnsuccessfulBuild() {
806 RunT r = getLastBuild();
807 while (r != null
808 && (r.isBuilding() || r.getResult() == Result.SUCCESS))
809 r = r.getPreviousBuild();
810 return r;
811 }
812
813 /**
814 * Returns the last unstable build, if any. Otherwise null.
815 * @see #getLastSuccessfulBuild
816 */
817 @Exported
818 @QuickSilver
819 public RunT getLastUnstableBuild() {
820 RunT r = getLastBuild();
821 while (r != null
822 && (r.isBuilding() || r.getResult() != Result.UNSTABLE))
823 r = r.getPreviousBuild();
824 return r;
825 }
826
827 /**
828 * Returns the last stable build, if any. Otherwise null.
829 * @see #getLastSuccessfulBuild
830 */
831 @Exported
832 @QuickSilver
833 public RunT getLastStableBuild() {
834 RunT r = getLastBuild();
835 while (r != null
836 && (r.isBuilding() || r.getResult().isWorseThan(Result.SUCCESS)))
837 r = r.getPreviousBuild();
838 return r;
839 }
840
841 /**
842 * Returns the last failed build, if any. Otherwise null.
843 */
844 @Exported
845 @QuickSilver
846 public RunT getLastFailedBuild() {
847 RunT r = getLastBuild();
848 while (r != null && (r.isBuilding() || r.getResult() != Result.FAILURE))
849 r = r.getPreviousBuild();
850 return r;
851 }
852
853 /**
854 * Returns the last completed build, if any. Otherwise null.
855 */
856 @Exported
857 @QuickSilver
858 public RunT getLastCompletedBuild() {
859 RunT r = getLastBuild();
860 while (r != null && r.isBuilding())
861 r = r.getPreviousBuild();
862 return r;
863 }
864
865 /**
866 * Returns the last 'numberOfBuilds' builds with a build result >= 'threshold'
867 *
868 * @return a list with the builds. May be smaller than 'numberOfBuilds' or even empty
869 * if not enough builds satisfying the threshold have been found. Never null.
870 */
871 public List<RunT> getLastBuildsOverThreshold(int numberOfBuilds, Result threshold) {
872
873 List<RunT> result = new ArrayList<RunT>(numberOfBuilds);
874
875 RunT r = getLastBuild();
876 while (r != null && result.size() < numberOfBuilds) {
877 if (!r.isBuilding() &&
878 (r.getResult() != null && r.getResult().isBetterOrEqualTo(threshold))) {
879 result.add(r);
880 }
881 r = r.getPreviousBuild();
882 }
883
884 return result;
885 }
886
887 public long getEstimatedDuration() {
888 List<RunT> builds = getLastBuildsOverThreshold(3, Result.UNSTABLE);
889
890 if(builds.isEmpty()) return -1;
891
892 long totalDuration = 0;
893 for (RunT b : builds) {
894 totalDuration += b.getDuration();
895 }
896 if(totalDuration==0) return -1;
897
898 return Math.round((double)totalDuration / builds.size());
899 }
900
901 /**
902 * Gets all the {@link Permalink}s defined for this job.
903 *
904 * @return never null
905 */
906 public PermalinkList getPermalinks() {
907 // TODO: shall we cache this?
908 PermalinkList permalinks = new PermalinkList(Permalink.BUILTIN);
909 for (Action a : getActions()) {
910 if (a instanceof PermalinkProjectAction) {
911 PermalinkProjectAction ppa = (PermalinkProjectAction) a;
912 permalinks.addAll(ppa.getPermalinks());
913 }
914 }
915 return permalinks;
916 }
917
918 /**
919 * Used as the color of the status ball for the project.
920 */
921 @Exported(visibility = 2, name = "color")
922 public BallColor getIconColor() {
923 RunT lastBuild = getLastBuild();
924 while (lastBuild != null && lastBuild.hasntStartedYet())
925 lastBuild = lastBuild.getPreviousBuild();
926
927 if (lastBuild != null)
928 return lastBuild.getIconColor();
929 else
930 return BallColor.GREY;
931 }
932
933 /**
934 * Get the current health report for a job.
935 *
936 * @return the health report. Never returns null
937 */
938 public HealthReport getBuildHealth() {
939 List<HealthReport> reports = getBuildHealthReports();
940 return reports.isEmpty() ? new HealthReport() : reports.get(0);
941 }
942
943 @Exported(name = "healthReport")
944 public List<HealthReport> getBuildHealthReports() {
945 List<HealthReport> reports = new ArrayList<HealthReport>();
946 RunT lastBuild = getLastBuild();
947
948 if (lastBuild != null && lastBuild.isBuilding()) {
949 // show the previous build's report until the current one is
950 // finished building.
951 lastBuild = lastBuild.getPreviousBuild();
952 }
953
954 // check the cache
955 if (cachedBuildHealthReportsBuildNumber != null
956 && cachedBuildHealthReports != null
957 && lastBuild != null
958 && cachedBuildHealthReportsBuildNumber.intValue() == lastBuild
959 .getNumber()) {
960 reports.addAll(cachedBuildHealthReports);
961 } else if (lastBuild != null) {
962 for (HealthReportingAction healthReportingAction : lastBuild
963 .getActions(HealthReportingAction.class)) {
964 final HealthReport report = healthReportingAction
965 .getBuildHealth();
966 if (report != null) {
967 if (report.isAggregateReport()) {
968 reports.addAll(report.getAggregatedReports());
969 } else {
970 reports.add(report);
971 }
972 }
973 }
974 final HealthReport report = getBuildStabilityHealthReport();
975 if (report != null) {
976 if (report.isAggregateReport()) {
977 reports.addAll(report.getAggregatedReports());
978 } else {
979 reports.add(report);
980 }
981 }
982
983 Collections.sort(reports);
984
985 // store the cache
986 cachedBuildHealthReportsBuildNumber = lastBuild.getNumber();
987 cachedBuildHealthReports = new ArrayList<HealthReport>(reports);
988 }
989
990 return reports;
991 }
992
993 private HealthReport getBuildStabilityHealthReport() {
994 // we can give a simple view of build health from the last five builds
995 int failCount = 0;
996 int totalCount = 0;
997 RunT i = getLastBuild();
998 while (totalCount < 5 && i != null) {
999 switch (i.getIconColor()) {
1000 case BLUE:
1001 case YELLOW:
1002 // failCount stays the same
1003 totalCount++;
1004 break;
1005 case RED:
1006 failCount++;
1007 totalCount++;
1008 break;
1009
1010 default:
1011 // do nothing as these are inconclusive statuses
1012 break;
1013 }
1014 i = i.getPreviousBuild();
1015 }
1016 if (totalCount > 0) {
1017 int score = (int) ((100.0 * (totalCount - failCount)) / totalCount);
1018
1019 Localizable description;
1020 if (failCount == 0) {
1021 description = Messages._Job_NoRecentBuildFailed();
1022 } else if (totalCount == failCount) {
1023 // this should catch the case where totalCount == 1
1024 // as failCount must be between 0 and totalCount
1025 // and we can't get here if failCount == 0
1026 description = Messages._Job_AllRecentBuildFailed();
1027 } else {
1028 description = Messages._Job_NOfMFailed(failCount, totalCount);
1029 }
1030 return new HealthReport(score, Messages._Job_BuildStability(description));
1031 }
1032 return null;
1033 }
1034
1035 //
1036 //
1037 // actions
1038 //
1039 //
1040 /**
1041 * Accepts submission from the configuration page.
1042 */
1043 @RequirePOST
1044 public synchronized void doConfigSubmit(StaplerRequest req,
1045 StaplerResponse rsp) throws IOException, ServletException, FormException {
1046 checkPermission(CONFIGURE);
1047
1048 description = req.getParameter("description");
1049
1050 keepDependencies = req.getParameter("keepDependencies") != null;
1051
1052 try {
1053 JSONObject json = req.getSubmittedForm();
1054
1055 setDisplayName(json.optString("displayNameOrNull"));
1056
1057 if (json.optBoolean("logrotate"))
1058 logRotator = req.bindJSON(BuildDiscarder.class, json.optJSONObject("buildDiscarder"));
1059 else
1060 logRotator = null;
1061
1062 DescribableList<JobProperty<?>, JobPropertyDescriptor> t = new DescribableList<JobProperty<?>, JobPropertyDescriptor>(NOOP,getAllProperties());
1063 t.rebuild(req,json.optJSONObject("properties"),JobPropertyDescriptor.getPropertyDescriptors(Job.this.getClass()));
1064 properties.clear();
1065 for (JobProperty p : t) {
1066 p.setOwner(this);
1067 properties.add(p);
1068 }
1069
1070 submit(req, rsp);
1071
1072 save();
1073 ItemListener.fireOnUpdated(this);
1074
1075 String newName = req.getParameter("name");
1076 final ProjectNamingStrategy namingStrategy = Jenkins.getInstance().getProjectNamingStrategy();
1077 if (newName != null && !newName.equals(name)) {
1078 // check this error early to avoid HTTP response splitting.
1079 Jenkins.checkGoodName(newName);
1080 namingStrategy.checkName(newName);
1081 rsp.sendRedirect("rename?newName=" + URLEncoder.encode(newName, "UTF-8"));
1082 } else {
1083 if(namingStrategy.isForceExistingJobs()){
1084 namingStrategy.checkName(name);
1085 }
1086 FormApply.success(".").generateResponse(req, rsp, null);
1087 }
1088 } catch (JSONException e) {
1089 StringWriter sw = new StringWriter();
1090 PrintWriter pw = new PrintWriter(sw);
1091 pw.println("Failed to parse form data. Please report this problem as a bug");
1092 pw.println("JSON=" + req.getSubmittedForm());
1093 pw.println();
1094 e.printStackTrace(pw);
1095
1096 rsp.setStatus(SC_BAD_REQUEST);
1097 sendError(sw.toString(), req, rsp, true);
1098 }
1099 }
1100
1101 /**
1102 * Derived class can override this to perform additional config submission
1103 * work.
1104 */
1105 protected void submit(StaplerRequest req, StaplerResponse rsp)
1106 throws IOException, ServletException, FormException {
1107 }
1108
1109 /**
1110 * Accepts and serves the job description
1111 */
1112 public void doDescription(StaplerRequest req, StaplerResponse rsp)
1113 throws IOException {
1114 if (req.getMethod().equals("GET")) {
1115 //read
1116 rsp.setContentType("text/plain;charset=UTF-8");
1117 rsp.getWriter().write(Util.fixNull(this.getDescription()));
1118 return;
1119 }
1120 if (req.getMethod().equals("POST")) {
1121 checkPermission(CONFIGURE);
1122
1123 // submission
1124 if (req.getParameter("description") != null) {
1125 this.setDescription(req.getParameter("description"));
1126 rsp.sendError(SC_NO_CONTENT);
1127 return;
1128 }
1129 }
1130
1131 // huh?
1132 rsp.sendError(SC_BAD_REQUEST);
1133 }
1134
1135 /**
1136 * Returns the image that shows the current buildCommand status.
1137 */
1138 public void doBuildStatus(StaplerRequest req, StaplerResponse rsp)
1139 throws IOException {
1140 rsp.sendRedirect2(req.getContextPath() + "/images/48x48/" + getBuildStatusUrl());
1141 }
1142
1143 public String getBuildStatusUrl() {
1144 return getIconColor().getImage();
1145 }
1146
1147 public Graph getBuildTimeGraph() {
1148 return new Graph(getLastBuild().getTimestamp(),500,400) {
1149 @Override
1150 protected JFreeChart createGraph() {
1151 class ChartLabel implements Comparable<ChartLabel> {
1152 final Run run;
1153
1154 public ChartLabel(Run r) {
1155 this.run = r;
1156 }
1157
1158 public int compareTo(ChartLabel that) {
1159 return this.run.number - that.run.number;
1160 }
1161
1162 @Override
1163 public boolean equals(Object o) {
1164 // HUDSON-2682 workaround for Eclipse compilation bug
1165 // on (c instanceof ChartLabel)
1166 if (o == null || !ChartLabel.class.isAssignableFrom( o.getClass() )) {
1167 return false;
1168 }
1169 ChartLabel that = (ChartLabel) o;
1170 return run == that.run;
1171 }
1172
1173 public Color getColor() {
1174 // TODO: consider gradation. See
1175 // http://www.javadrive.jp/java2d/shape/index9.html
1176 Result r = run.getResult();
1177 if (r == Result.FAILURE)
1178 return ColorPalette.RED;
1179 else if (r == Result.UNSTABLE)
1180 return ColorPalette.YELLOW;
1181 else if (r == Result.ABORTED || r == Result.NOT_BUILT)
1182 return ColorPalette.GREY;
1183 else
1184 return ColorPalette.BLUE;
1185 }
1186
1187 @Override
1188 public int hashCode() {
1189 return run.hashCode();
1190 }
1191
1192 @Override
1193 public String toString() {
1194 String l = run.getDisplayName();
1195 if (run instanceof Build) {
1196 String s = ((Build) run).getBuiltOnStr();
1197 if (s != null)
1198 l += ' ' + s;
1199 }
1200 return l;
1201 }
1202
1203 }
1204
1205 DataSetBuilder<String, ChartLabel> data = new DataSetBuilder<String, ChartLabel>();
1206 for (Run r : getBuilds()) {
1207 if (r.isBuilding())
1208 continue;
1209 data.add(((double) r.getDuration()) / (1000 * 60), "min",
1210 new ChartLabel(r));
1211 }
1212
1213 final CategoryDataset dataset = data.build();
1214
1215 final JFreeChart chart = ChartFactory.createStackedAreaChart(null, // chart
1216 // title
1217 null, // unused
1218 Messages.Job_minutes(), // range axis label
1219 dataset, // data
1220 PlotOrientation.VERTICAL, // orientation
1221 false, // include legend
1222 true, // tooltips
1223 false // urls
1224 );
1225
1226 chart.setBackgroundPaint(Color.white);
1227
1228 final CategoryPlot plot = chart.getCategoryPlot();
1229
1230 // plot.setAxisOffset(new Spacer(Spacer.ABSOLUTE, 5.0, 5.0, 5.0, 5.0));
1231 plot.setBackgroundPaint(Color.WHITE);
1232 plot.setOutlinePaint(null);
1233 plot.setForegroundAlpha(0.8f);
1234 // plot.setDomainGridlinesVisible(true);
1235 // plot.setDomainGridlinePaint(Color.white);
1236 plot.setRangeGridlinesVisible(true);
1237 plot.setRangeGridlinePaint(Color.black);
1238
1239 CategoryAxis domainAxis = new ShiftedCategoryAxis(null);
1240 plot.setDomainAxis(domainAxis);
1241 domainAxis.setCategoryLabelPositions(CategoryLabelPositions.UP_90);
1242 domainAxis.setLowerMargin(0.0);
1243 domainAxis.setUpperMargin(0.0);
1244 domainAxis.setCategoryMargin(0.0);
1245
1246 final NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis();
1247 ChartUtil.adjustChebyshev(dataset, rangeAxis);
1248 rangeAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits());
1249
1250 StackedAreaRenderer ar = new StackedAreaRenderer2() {
1251 @Override
1252 public Paint getItemPaint(int row, int column) {
1253 ChartLabel key = (ChartLabel) dataset.getColumnKey(column);
1254 return key.getColor();
1255 }
1256
1257 @Override
1258 public String generateURL(CategoryDataset dataset, int row,
1259 int column) {
1260 ChartLabel label = (ChartLabel) dataset.getColumnKey(column);
1261 return String.valueOf(label.run.number);
1262 }
1263
1264 @Override
1265 public String generateToolTip(CategoryDataset dataset, int row,
1266 int column) {
1267 ChartLabel label = (ChartLabel) dataset.getColumnKey(column);
1268 return label.run.getDisplayName() + " : "
1269 + label.run.getDurationString();
1270 }
1271 };
1272 plot.setRenderer(ar);
1273
1274 // crop extra space around the graph
1275 plot.setInsets(new RectangleInsets(0, 0, 0, 5.0));
1276
1277 return chart;
1278 }
1279 };
1280 }
1281
1282 /**
1283 * Renames this job.
1284 */
1285 @RequirePOST
1286 public/* not synchronized. see renameTo() */void doDoRename(
1287 StaplerRequest req, StaplerResponse rsp) throws IOException,
1288 ServletException {
1289
1290 if (!hasPermission(CONFIGURE)) {
1291 // rename is essentially delete followed by a create
1292 checkPermission(CREATE);
1293 checkPermission(DELETE);
1294 }
1295
1296 String newName = req.getParameter("newName");
1297 Jenkins.checkGoodName(newName);
1298
1299 if (isBuilding()) {
1300 // redirect to page explaining that we can't rename now
1301 rsp.sendRedirect("rename?newName=" + URLEncoder.encode(newName, "UTF-8"));
1302 return;
1303 }
1304
1305 renameTo(newName);
1306 // send to the new job page
1307 // note we can't use getUrl() because that would pick up old name in the
1308 // Ancestor.getUrl()
1309 rsp.sendRedirect2("../" + newName);
1310 }
1311
1312 public void doRssAll(StaplerRequest req, StaplerResponse rsp)
1313 throws IOException, ServletException {
1314 rss(req, rsp, " all builds", getBuilds());
1315 }
1316
1317 public void doRssFailed(StaplerRequest req, StaplerResponse rsp)
1318 throws IOException, ServletException {
1319 rss(req, rsp, " failed builds", getBuilds().failureOnly());
1320 }
1321
1322 private void rss(StaplerRequest req, StaplerResponse rsp, String suffix,
1323 RunList runs) throws IOException, ServletException {
1324 RSS.forwardToRss(getDisplayName() + suffix, getUrl(), runs.newBuilds(),
1325 Run.FEED_ADAPTER, req, rsp);
1326 }
1327
1328 /**
1329 * Returns the {@link ACL} for this object.
1330 * We need to override the identical method in AbstractItem because we won't
1331 * call getACL(Job) otherwise (single dispatch)
1332 */
1333 @Override
1334 public ACL getACL() {
1335 return Jenkins.getInstance().getAuthorizationStrategy().getACL(this);
1336 }
1337
1338 public BuildTimelineWidget getTimeline() {
1339 return new BuildTimelineWidget(getBuilds());
1340 }
1341
1342 private final static HexStringConfidentialKey SERVER_COOKIE = new HexStringConfidentialKey(Job.class,"serverCookie",16);
1343 }
1344