AbstractProject.java 34.5 KB
Newer Older
1 2
package hudson.model;

K
kohsuke 已提交
3
import hudson.AbortException;
4
import hudson.FeedAdapter;
5
import hudson.FilePath;
6
import hudson.Launcher;
7
import hudson.StructuredForm;
8
import hudson.maven.MavenModule;
9 10 11
import hudson.model.Descriptor.FormException;
import hudson.model.Fingerprint.RangeSet;
import hudson.model.RunMap.Constructor;
K
kohsuke 已提交
12
import hudson.scm.ChangeLogSet;
K
kohsuke 已提交
13
import hudson.scm.ChangeLogSet.Entry;
14
import hudson.scm.NullSCM;
15
import hudson.scm.SCM;
K
kohsuke 已提交
16
import hudson.scm.SCMS;
J
jbq 已提交
17
import hudson.search.SearchIndexBuilder;
H
huybrechts 已提交
18
import hudson.security.ACL;
K
kohsuke 已提交
19
import hudson.security.Permission;
J
jbq 已提交
20
import hudson.tasks.BuildTrigger;
21
import hudson.tasks.Publisher;
K
kohsuke 已提交
22
import hudson.tasks.BuildStep;
K
kohsuke 已提交
23
import hudson.triggers.SCMTrigger;
24
import hudson.triggers.Trigger;
25
import hudson.triggers.TriggerDescriptor;
26
import hudson.triggers.Triggers;
27
import hudson.util.EditDistance;
28
import hudson.util.DescribableList;
K
kohsuke 已提交
29 30 31 32 33 34
import hudson.widgets.BuildHistoryWidget;
import hudson.widgets.HistoryWidget;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.export.Exported;
35

K
kohsuke 已提交
36
import javax.servlet.ServletException;
37
import java.io.File;
38
import java.io.IOException;
K
kohsuke 已提交
39
import java.lang.reflect.InvocationTargetException;
K
kohsuke 已提交
40
import java.util.ArrayList;
41
import java.util.Calendar;
42
import java.util.Collection;
J
jbq 已提交
43
import java.util.Collections;
44
import java.util.Comparator;
J
jbq 已提交
45
import java.util.HashSet;
46 47
import java.util.List;
import java.util.Map;
J
jbq 已提交
48
import java.util.Set;
49 50 51
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.Vector;
52 53
import java.util.logging.Level;
import java.util.logging.Logger;
54 55 56 57

/**
 * Base implementation of {@link Job}s that build software.
 *
58
 * For now this is primarily the common part of {@link Project} and {@link MavenModule}.
59 60 61 62
 *
 * @author Kohsuke Kawaguchi
 * @see AbstractBuild
 */
63
public abstract class AbstractProject<P extends AbstractProject<P,R>,R extends AbstractBuild<P,R>> extends Job<P,R> implements BuildableItem {
64

65
	/**
66 67 68 69
     * {@link SCM} associated with the project.
     * To allow derived classes to link {@link SCM} config to elsewhere,
     * access to this variable should always go through {@link #getScm()}.
     */
K
kohsuke 已提交
70
    private volatile SCM scm = new NullSCM();
71

72 73 74 75 76 77 78 79
    /**
     * All the builds keyed by their build number.
     */
    protected transient /*almost final*/ RunMap<R> builds = new RunMap<R>();

    /**
     * The quiet period. Null to delegate to the system default.
     */
K
kohsuke 已提交
80
    private volatile Integer quietPeriod = null;
81 82

    /**
83 84 85 86 87
     * If this project is configured to be only built on a certain label,
     * this value will be set to that label.
     *
     * For historical reasons, this is called 'assignedNode'. Also for
     * a historical reason, null to indicate the affinity
88 89
     * with the master node.
     *
90
     * @see #canRoam
91 92 93 94 95 96 97 98 99 100
     */
    private String assignedNode;

    /**
     * True if this project can be built on any node.
     *
     * <p>
     * This somewhat ugly flag combination is so that we can migrate
     * existing Hudson installations nicely.
     */
K
kohsuke 已提交
101
    private volatile boolean canRoam;
102 103 104 105

    /**
     * True to suspend new builds.
     */
K
kohsuke 已提交
106
    protected volatile boolean disabled;
107 108 109 110 111 112 113 114 115 116 117

    /**
     * Identifies {@link JDK} to be used.
     * Null if no explicit configuration is required.
     *
     * <p>
     * Can't store {@link JDK} directly because {@link Hudson} and {@link Project}
     * are saved independently.
     *
     * @see Hudson#getJDK(String)
     */
K
kohsuke 已提交
118
    private volatile String jdk;
119

120 121 122 123
    /**
     * @deprecated
     */
    private transient boolean enableRemoteTrigger;
124

K
kohsuke 已提交
125
    private volatile BuildAuthorizationToken authToken = null;
126

127 128 129
    /**
     * List of all {@link Trigger}s for this project.
     */
130
    protected List<Trigger<?>> triggers = new Vector<Trigger<?>>();
131

132 133 134 135 136 137 138 139 140
    /**
     * {@link Action}s contributed from subsidiary objects associated with
     * {@link AbstractProject}, such as from triggers, builders, publishers, etc.
     *
     * We don't want to persist them separately, and these actions
     * come and go as configuration change, so it's kept separate.
     */
    protected transient /*final*/ List<Action> transientActions = new Vector<Action>();

141 142
    protected AbstractProject(ItemGroup parent, String name) {
        super(parent,name);
143

144
        if(!Hudson.getInstance().getSlaves().isEmpty()) {
145 146 147 148 149 150
            // if a new job is configured with Hudson that already has slave nodes
            // make it roamable by default
            canRoam = true;
        }
    }

151
    @Override
152 153
    public void onLoad(ItemGroup<? extends Item> parent, String name) throws IOException {
        super.onLoad(parent, name);
154 155 156 157 158 159 160 161 162 163

        this.builds = new RunMap<R>();
        this.builds.load(this,new Constructor<R>() {
            public R create(File dir) throws IOException {
                return loadBuild(dir);
            }
        });

        if(triggers==null)
            // it didn't exist in < 1.28
164
            triggers = new Vector<Trigger<?>>();
165 166
        for (Trigger t : triggers)
            t.start(this,false);
167 168 169

        if(transientActions==null)
            transientActions = new Vector<Action>();    // happens when loaded from disk
170
        updateTransientActions();
171 172
    }

K
kohsuke 已提交
173 174 175 176 177 178
    protected void performDelete() throws IOException {
        // prevent a new build while a delete operation is in progress
        makeDisabled(true);
        super.performDelete();
    }

179 180 181 182
    /**
     * If this project is configured to be always built on this node,
     * return that {@link Node}. Otherwise null.
     */
183
    public Label getAssignedLabel() {
184 185 186
        if(canRoam)
            return null;

187 188 189
        if(assignedNode==null)
            return Hudson.getInstance().getSelfLabel();
        return Hudson.getInstance().getLabel(assignedNode);
190 191
    }

192 193 194 195 196 197
    /**
     * Get the term used in the UI to represent this kind of {@link AbstractProject}.
     * Must start with a capital letter.
     */
    @Override
    public String getPronoun() {
198
        return Messages.AbstractProject_Pronoun();
199 200
    }

201 202
    /**
     * Gets the directory where the module is checked out.
K
kohsuke 已提交
203 204 205
     *
     * @return
     *      null if the workspace is on a slave that's not connected.
206
     */
207
    public abstract FilePath getWorkspace();
208

209 210 211 212
    /**
     * Returns the root directory of the checked-out module.
     * <p>
     * This is usually where <tt>pom.xml</tt>, <tt>build.xml</tt>
213
     * and so on exists.
214 215
     */
    public FilePath getModuleRoot() {
K
kohsuke 已提交
216 217 218
        FilePath ws = getWorkspace();
        if(ws==null)    return null;
        return getScm().getModuleRoot(ws);
219 220
    }

S
stephenconnolly 已提交
221 222 223 224 225 226 227 228 229 230 231
    /**
     * Returns the root directories of all checked-out modules.
     * <p>
     * Some SCMs support checking out multiple modules into the same workspace.
     * In these cases, the returned array will have a length greater than one.
     * @return The roots of all modules checked out from the SCM.
     */
    public FilePath[] getModuleRoots() {
        return getScm().getModuleRoots(getWorkspace());
    }

232
    public int getQuietPeriod() {
233
        return quietPeriod!=null ? quietPeriod : Hudson.getInstance().getQuietPeriod();
234 235 236 237 238 239 240 241
    }

    // ugly name because of EL
    public boolean getHasCustomQuietPeriod() {
        return quietPeriod!=null;
    }

    public final boolean isBuildable() {
K
kohsuke 已提交
242
        return !isDisabled();
243 244
    }

245 246 247 248 249 250 251 252
    /**
     * Used in <tt>sidepanel.jelly</tt> to decide whether to display
     * the config/delete/build links.
     */
    public boolean isConfigurable() {
        return true;
    }

253 254 255 256
    public boolean isDisabled() {
        return disabled;
    }

257 258 259 260
    /**
     * Marks the build as disabled.
     */
    public void makeDisabled(boolean b) throws IOException {
K
kohsuke 已提交
261
        if(disabled==b)     return; // noop
262 263 264 265
        this.disabled = b;
        save();
    }

K
kohsuke 已提交
266 267 268
    @Override
    public BallColor getIconColor() {
        if(isDisabled())
269
            return BallColor.DISABLED;
K
kohsuke 已提交
270 271 272
        else
            return super.getIconColor();
    }
273

274 275 276
    protected void updateTransientActions() {
        synchronized(transientActions) {
            transientActions.clear();
277

278 279 280 281 282 283 284 285
            for (JobProperty<? super P> p : properties) {
                Action a = p.getJobAction((P)this);
                if(a!=null)
                    transientActions.add(a);
            }
        }
    }

286 287 288 289 290 291 292 293 294
    /**
     * Returns the live list of all {@link Publisher}s configured for this project.
     *
     * <p>
     * This method couldn't be called <tt>getPublishers()</tt> because existing methods
     * in sub-classes return different inconsistent types.
     */
    public abstract DescribableList<Publisher,Descriptor<Publisher>> getPublishersList();

K
kohsuke 已提交
295 296 297 298 299 300
    @Override
    public void addProperty(JobProperty<? super P> jobProp) throws IOException {
        super.addProperty(jobProp);
        updateTransientActions();
    }

301 302 303 304 305 306 307 308 309 310
    public List<ProminentProjectAction> getProminentActions() {
        List<Action> a = getActions();
        List<ProminentProjectAction> pa = new Vector<ProminentProjectAction>();
        for (Action action : a) {
            if(action instanceof ProminentProjectAction)
                pa.add((ProminentProjectAction) action);
        }
        return pa;
    }

311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326
    @Override
    public void doConfigSubmit( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
        super.doConfigSubmit(req,rsp);

        Set<AbstractProject> upstream = Collections.emptySet();
        if(req.getParameter("pseudoUpstreamTrigger")!=null) {
            upstream = new HashSet<AbstractProject>(Items.fromNameList(req.getParameter("upstreamProjects"),AbstractProject.class));
        }

        // dependency setting might have been changed by the user, so rebuild.
        Hudson.getInstance().rebuildDependencyGraph();

        // reflect the submission of the pseudo 'upstream build trriger'.
        // this needs to be done after we release the lock on 'this',
        // or otherwise we could dead-lock

327
        for (AbstractProject<?,?> p : Hudson.getInstance().getAllItems(AbstractProject.class)) {
328 329
            boolean isUpstream = upstream.contains(p);
            synchronized(p) {
K
kohsuke 已提交
330
                // does 'p' include us in its BuildTrigger? 
331 332
                DescribableList<Publisher,Descriptor<Publisher>> pl = p.getPublishersList();
                BuildTrigger trigger = (BuildTrigger) pl.get(BuildTrigger.DESCRIPTOR);
K
kohsuke 已提交
333
                List<AbstractProject> newChildProjects = trigger == null ? new ArrayList<AbstractProject>():trigger.getChildProjects();
334 335 336 337 338 339 340 341
                if(isUpstream) {
                    if(!newChildProjects.contains(this))
                        newChildProjects.add(this);
                } else {
                    newChildProjects.remove(this);
                }

                if(newChildProjects.isEmpty()) {
342
                    pl.remove(BuildTrigger.DESCRIPTOR);
343
                } else {
344
                    BuildTrigger existing = (BuildTrigger)pl.get(BuildTrigger.DESCRIPTOR);
345 346
                    if(existing!=null && existing.hasSame(newChildProjects))
                        continue;   // no need to touch
347
                    pl.add(new BuildTrigger(newChildProjects,
348 349 350 351 352 353 354 355 356 357 358 359
                        existing==null?Result.SUCCESS:existing.getThreshold()));
                }
            }
        }

        // notify the queue as the project might be now tied to different node
        Hudson.getInstance().getQueue().scheduleMaintenance();

        // this is to reflect the upstream build adjustments done above
        Hudson.getInstance().rebuildDependencyGraph();
    }

360 361
    /**
     * Schedules a build of this project.
362 363 364 365 366
     *
     * @return
     *      true if the project is actually added to the queue.
     *      false if the queue contained it and therefore the add()
     *      was noop
367
     */
368 369
    public boolean scheduleBuild() {
        if(isDisabled())    return false;
K
kohsuke 已提交
370
        return Hudson.getInstance().getQueue().add(this, getQuietPeriod());
371 372
    }

373 374 375 376 377 378 379 380 381 382 383
    /**
     * Schedules a polling of this project.
     */
    public boolean schedulePolling() {
        if(isDisabled())    return false;
        SCMTrigger scmt = getTrigger(SCMTrigger.class);
        if(scmt==null)      return false;
        scmt.run();
        return true;
    }

384 385 386 387 388
    /**
     * Returns true if the build is in the queue.
     */
    @Override
    public boolean isInQueue() {
389
        return Hudson.getInstance().getQueue().contains(this);
390 391
    }

K
kohsuke 已提交
392 393 394 395 396
    @Override
    public Queue.Item getQueueItem() {
        return Hudson.getInstance().getQueue().getItem(this);
    }

397 398 399 400 401 402 403 404
    /**
     * Returns true if a build of this project is in progress.
     */
    public boolean isBuilding() {
        R b = getLastBuild();
        return b!=null && b.isBuilding();
    }

K
kohsuke 已提交
405 406 407
    /**
     * Gets the JDK that this project is configured with, or null.
     */
408
    public JDK getJDK() {
409
        return Hudson.getInstance().getJDK(jdk);
410 411 412 413 414 415 416 417 418 419
    }

    /**
     * Overwrites the JDK setting.
     */
    public synchronized void setJDK(JDK jdk) throws IOException {
        this.jdk = jdk.getName();
        save();
    }

420 421
    public BuildAuthorizationToken getAuthToken() {
        return authToken;
422 423 424 425 426 427 428 429 430 431
    }

    public SortedMap<Integer, ? extends R> _getRuns() {
        return builds.getView();
    }

    public void removeRun(R run) {
        this.builds.remove(run);
    }

432 433 434 435 436
    /**
     * Determines Class&lt;R>.
     */
    protected abstract Class<R> getBuildClass();

437 438 439
    /**
     * Creates a new build of this project for immediate execution.
     */
440 441 442 443 444 445 446 447 448 449
    protected R newBuild() throws IOException {
        try {
            R lastBuild = getBuildClass().getConstructor(getClass()).newInstance(this);
            builds.put(lastBuild);
            return lastBuild;
        } catch (InstantiationException e) {
            throw new Error(e);
        } catch (IllegalAccessException e) {
            throw new Error(e);
        } catch (InvocationTargetException e) {
450
            throw handleInvocationTargetException(e);
451 452 453 454
        } catch (NoSuchMethodException e) {
            throw new Error(e);
        }
    }
455

456 457 458 459 460 461 462 463
    private IOException handleInvocationTargetException(InvocationTargetException e) {
        Throwable t = e.getTargetException();
        if(t instanceof Error)  throw (Error)t;
        if(t instanceof RuntimeException)   throw (RuntimeException)t;
        if(t instanceof IOException)    return (IOException)t;
        throw new Error(t);
    }

464 465 466
    /**
     * Loads an existing build record from disk.
     */
467 468 469 470 471 472 473 474
    protected R loadBuild(File dir) throws IOException {
        try {
            return getBuildClass().getConstructor(getClass(),File.class).newInstance(this,dir);
        } catch (InstantiationException e) {
            throw new Error(e);
        } catch (IllegalAccessException e) {
            throw new Error(e);
        } catch (InvocationTargetException e) {
475
            throw handleInvocationTargetException(e);
476 477 478 479
        } catch (NoSuchMethodException e) {
            throw new Error(e);
        }
    }
480

K
kohsuke 已提交
481 482 483 484 485 486 487 488
    /**
     * {@inheritDoc}
     *
     * <p>
     * Note that this method returns a read-only view of {@link Action}s.
     * {@link BuildStep}s and others who want to add a project action
     * should do so by implementing {@link BuildStep#getProjectAction(AbstractProject)}.
     */
489 490 491 492
    public synchronized List<Action> getActions() {
        // add all the transient actions, too
        List<Action> actions = new Vector<Action>(super.getActions());
        actions.addAll(transientActions);
K
kohsuke 已提交
493 494
        // return the read only list to cause a failure on plugins who try to add an action here
        return Collections.unmodifiableList(actions);
495 496
    }

497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512
    /**
     * Gets the {@link Node} where this project was last built on.
     *
     * @return
     *      null if no information is available (for example,
     *      if no build was done yet.)
     */
    public Node getLastBuiltOn() {
        // where was it built on?
        AbstractBuild b = getLastBuild();
        if(b==null)
            return null;
        else
            return b.getBuiltOn();
    }

513
    /**
514
     * {@inheritDoc}
515 516 517 518 519
     *
     * <p>
     * A project must be blocked if its own previous build is in progress,
     * but derived classes can also check other conditions.
     */
520
    public boolean isBuildBlocked() {
521 522 523
        return isBuilding();
    }

524 525 526 527 528
    public String getWhyBlocked() {
        AbstractBuild<?, ?> build = getLastBuild();
        Executor e = build.getExecutor();
        String eta="";
        if(e!=null)
K
i18n  
kohsuke 已提交
529
            eta = Messages.AbstractProject_ETA(e.getEstimatedRemainingTime());
530
        int lbn = build.getNumber();
K
i18n  
kohsuke 已提交
531
        return Messages.AbstractProject_BuildInProgress(lbn,eta);
532 533 534 535 536 537 538 539 540 541 542 543
    }

    public final long getEstimatedDuration() {
        AbstractBuild b = getLastSuccessfulBuild();
        if(b==null)     return -1;

        long duration = b.getDuration();
        if(duration==0) return -1;

        return duration;
    }

544 545
    public R createExecutable() throws IOException {
        return newBuild();
546 547
    }

548 549 550 551
    public void checkAbortPermission() {
        checkPermission(AbstractProject.ABORT);
    }

K
kohsuke 已提交
552 553 554 555
    public boolean hasAbortPermission() {
        return hasPermission(AbstractProject.ABORT);
    }

556 557 558 559 560 561 562 563 564 565 566
    /**
     * Gets the {@link Resource} that represents the workspace of this project.
     */
    public Resource getWorkspaceResource() {
        return new Resource(getFullDisplayName()+" workspace");
    }

    /**
     * List of necessary resources to perform the build of this project.
     */
    public ResourceList getResourceList() {
567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583
        final Set<ResourceActivity> resourceActivities = getResourceActivities();
        final List<ResourceList> resourceLists = new ArrayList<ResourceList>(1 + resourceActivities.size());
        for (ResourceActivity activity : resourceActivities) {
            if (activity != this && activity != null) {
                // defensive infinite recursion and null check
                resourceLists.add(activity.getResourceList());
            }
        }
        resourceLists.add(new ResourceList().w(getWorkspaceResource()));
        return ResourceList.union(resourceLists);
    }

    /**
     * Set of child resource activities of the build of this project (override in child projects).
     * @return The set of child resource activities of the build of this project.
     */
    protected Set<ResourceActivity> getResourceActivities() {
K
kohsuke 已提交
584
        return Collections.emptySet();
585 586
    }

587
    public boolean checkout(AbstractBuild build, Launcher launcher, BuildListener listener, File changelogFile) throws IOException {
588
        SCM scm = getScm();
589 590 591 592 593 594 595 596 597
        if(scm==null)
            return true;    // no SCM

        try {
            FilePath workspace = getWorkspace();
            workspace.mkdirs();

            return scm.checkout(build, launcher, workspace, listener, changelogFile);
        } catch (InterruptedException e) {
K
i18n  
kohsuke 已提交
598
            listener.getLogger().println(Messages.AbstractProject_ScmAborted());
599
            LOGGER.log(Level.INFO,build.toString()+" aborted",e);
600 601 602 603 604 605 606 607 608 609 610 611
            return false;
        }
    }

    /**
     * Checks if there's any update in SCM, and returns true if any is found.
     *
     * <p>
     * The caller is responsible for coordinating the mutual exclusion between
     * a build and polling, as both touches the workspace.
     */
    public boolean pollSCMChanges( TaskListener listener ) {
612
        SCM scm = getScm();
613
        if(scm==null) {
K
i18n  
kohsuke 已提交
614
            listener.getLogger().println(Messages.AbstractProject_NoSCM());
615 616 617
            return false;
        }
        if(isDisabled()) {
K
i18n  
kohsuke 已提交
618
            listener.getLogger().println(Messages.AbstractProject_Disabled());
619
            return false;
620 621 622
        }

        try {
K
kohsuke 已提交
623 624
            FilePath workspace = getWorkspace();
            if (scm.requiresWorkspaceForPolling() && (workspace == null || !workspace.exists())) {
K
kohsuke 已提交
625
                // workspace offline. build now, or nothing will ever be built
K
kohsuke 已提交
626
                Label label = getAssignedLabel();
K
kohsuke 已提交
627
                if (label != null && label.isSelfLabel()) {
K
kohsuke 已提交
628 629 630 631 632
                    // if the build is fixed on a node, then attempting a build will do us
                    // no good. We should just wait for the slave to come back.
                    listener.getLogger().println(Messages.AbstractProject_NoWorkspace());
                    return false;
                }
K
kohsuke 已提交
633
                if (workspace == null)
K
kohsuke 已提交
634 635 636 637 638 639
                    listener.getLogger().println(Messages.AbstractProject_WorkspaceOffline());
                else
                    listener.getLogger().println(Messages.AbstractProject_NoWorkspace());
                listener.getLogger().println(Messages.AbstractProject_NewBuildForWorkspace());
                return true;
            }
K
kohsuke 已提交
640

K
kohsuke 已提交
641 642 643
            Launcher launcher = workspace != null ? workspace.createLauncher(listener) : null;
            LOGGER.fine("Polling SCM changes of " + getName());
            return scm.pollChanges(this, launcher, workspace, listener);
644
        } catch (AbortException e) {
K
i18n  
kohsuke 已提交
645
            listener.fatalError(Messages.AbstractProject_Aborted());
646
            return false;
647 648 649 650
        } catch (IOException e) {
            e.printStackTrace(listener.fatalError(e.getMessage()));
            return false;
        } catch (InterruptedException e) {
K
i18n  
kohsuke 已提交
651
            e.printStackTrace(listener.fatalError(Messages.AbstractProject_PollingABorted()));
652 653 654 655
            return false;
        }
    }

656 657 658 659 660 661 662 663 664 665 666 667
    /**
     * Returns true if this user has made a commit to this project.
     *
     * @since 1.191
     */
    public boolean hasParticipant(User user) {
        for( R build = getLastBuild(); build!=null; build=build.getPreviousBuild())
            if(build.hasParticipant(user))
                return true;
        return false;
    }

668 669 670 671 672 673 674 675
    public SCM getScm() {
        return scm;
    }

    public void setScm(SCM scm) {
        this.scm = scm;
    }

676 677 678
    /**
     * Adds a new {@link Trigger} to this {@link Project} if not active yet.
     */
679
    public void addTrigger(Trigger<?> trigger) throws IOException {
680 681 682
        addToList(trigger,triggers);
    }

683
    public void removeTrigger(TriggerDescriptor trigger) throws IOException {
684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713
        removeFromList(trigger,triggers);
    }

    protected final synchronized <T extends Describable<T>>
    void addToList( T item, List<T> collection ) throws IOException {
        for( int i=0; i<collection.size(); i++ ) {
            if(collection.get(i).getDescriptor()==item.getDescriptor()) {
                // replace
                collection.set(i,item);
                save();
                return;
            }
        }
        // add
        collection.add(item);
        save();
    }

    protected final synchronized <T extends Describable<T>>
    void removeFromList(Descriptor<T> item, List<T> collection) throws IOException {
        for( int i=0; i< collection.size(); i++ ) {
            if(collection.get(i).getDescriptor()==item) {
                // found it
                collection.remove(i);
                save();
                return;
            }
        }
    }

714 715
    public synchronized Map<TriggerDescriptor,Trigger> getTriggers() {
        return (Map)Descriptor.toMap(triggers);
716 717
    }

718 719 720 721 722 723 724 725 726 727 728
    /**
     * Gets the specific trigger, or null if the propert is not configured for this job.
     */
    public <T extends Trigger> T getTrigger(Class<T> clazz) {
        for (Trigger p : triggers) {
            if(clazz.isInstance(p))
                return clazz.cast(p);
        }
        return null;
    }

729 730 731 732 733 734 735 736 737 738 739 740 741 742
//
//
// fingerprint related
//
//
    /**
     * True if the builds of this project produces {@link Fingerprint} records.
     */
    public abstract boolean isFingerprintConfigured();

    /**
     * Gets the other {@link AbstractProject}s that should be built
     * when a build of this project is completed.
     */
K
kohsuke 已提交
743
    @Exported
744 745 746
    public final List<AbstractProject> getDownstreamProjects() {
        return Hudson.getInstance().getDependencyGraph().getDownstream(this);
    }
747

K
kohsuke 已提交
748
    @Exported
749 750
    public final List<AbstractProject> getUpstreamProjects() {
        return Hudson.getInstance().getDependencyGraph().getUpstream(this);
K
kohsuke 已提交
751 752
    }

K
kohsuke 已提交
753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774
    /**
     * Returns only those upstream projects that defines {@link BuildTrigger} to this project.
     * This is a subset of {@link #getUpstreamProjects()}
     *
     * @return A List of upstream projects that has a {@link BuildTrigger} to this project.
     */
    public final List<AbstractProject> getBuildTriggerUpstreamProjects() {
        ArrayList<AbstractProject> result = new ArrayList<AbstractProject>();
        for (AbstractProject ap : getUpstreamProjects()) {
            if (ap instanceof Project) {
                Project p = (Project) ap;
                BuildTrigger buildTrigger = (BuildTrigger)p.getPublisher(BuildTrigger.DESCRIPTOR);
                if (buildTrigger != null) {
                    if (buildTrigger.getChildProjects().contains(this)) {
                        result.add(p);
                    }
                }                
            }
        }        
        return result;
    }    
    
K
kohsuke 已提交
775 776 777 778 779 780 781 782 783 784 785 786 787 788 789
    /**
     * Gets all the upstream projects including transitive upstream projects.
     *
     * @since 1.138
     */
    public final Set<AbstractProject> getTransitiveUpstreamProjects() {
        return Hudson.getInstance().getDependencyGraph().getTransitiveUpstream(this);
    }

    /**
     * Gets all the downstream projects including transitive downstream projects.
     *
     * @since 1.138
     */
    public final Set<AbstractProject> getTransitiveDownstreamProjects() {
790
        return Hudson.getInstance().getDependencyGraph().getTransitiveDownstream(this);
791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830
    }

    /**
     * Gets the dependency relationship map between this project (as the source)
     * and that project (as the sink.)
     *
     * @return
     *      can be empty but not null. build number of this project to the build
     *      numbers of that project.
     */
    public SortedMap<Integer, RangeSet> getRelationship(AbstractProject that) {
        TreeMap<Integer,RangeSet> r = new TreeMap<Integer,RangeSet>(REVERSE_INTEGER_COMPARATOR);

        checkAndRecord(that, r, this.getBuilds());
        // checkAndRecord(that, r, that.getBuilds());

        return r;
    }

    /**
     * Helper method for getDownstreamRelationship.
     *
     * For each given build, find the build number range of the given project and put that into the map.
     */
    private void checkAndRecord(AbstractProject that, TreeMap<Integer, RangeSet> r, Collection<R> builds) {
        for (R build : builds) {
            RangeSet rs = build.getDownstreamRelationship(that);
            if(rs==null || rs.isEmpty())
                continue;

            int n = build.getNumber();

            RangeSet value = r.get(n);
            if(value==null)
                r.put(n,rs);
            else
                value.add(rs);
        }
    }

831 832 833 834 835 836
    /**
     * Builds the dependency graph.
     * @see DependencyGraph
     */
    protected abstract void buildDependencyGraph(DependencyGraph graph);

K
kohsuke 已提交
837 838 839 840 841 842 843
    protected SearchIndexBuilder makeSearchIndex() {
        SearchIndexBuilder sib = super.makeSearchIndex();
        if(isBuildable() && Hudson.isAdmin())
            sib.add("build","build");
        return sib;
    }

844 845 846 847
    @Override
    protected HistoryWidget createHistoryWidget() {
        return new BuildHistoryWidget<R>(this,getBuilds(),HISTORY_ADAPTER);
    }
K
kohsuke 已提交
848 849 850 851
    
    public boolean isParameterized() {
    	return getProperty(ParametersDefinitionProperty.class) != null;
    }
852

853 854 855 856 857 858 859 860 861
//
//
// actions
//
//
    /**
     * Schedules a new build command.
     */
    public void doBuild( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
862
        BuildAuthorizationToken.checkPermission(this, authToken, req, rsp);
863

K
kohsuke 已提交
864 865 866 867 868 869 870
        // if a build is parameterized, let that take over
        ParametersDefinitionProperty pp = getProperty(ParametersDefinitionProperty.class);
        if (pp != null) {
            pp._doBuild(req,rsp);
            return;
        }

871 872 873 874 875 876 877 878 879 880 881 882 883 884 885
        String delay = req.getParameter("delay");
        if (delay!=null) {
            if (!isDisabled()) {
                try {
                    // TODO: more unit handling
                    if(delay.endsWith("sec"))   delay=delay.substring(0,delay.length()-3);
                    if(delay.endsWith("secs"))  delay=delay.substring(0,delay.length()-4);
                    Hudson.getInstance().getQueue().add(this, Integer.parseInt(delay));
                } catch (NumberFormatException e) {
                    throw new ServletException("Invalid delay parameter value: "+delay);
                }
            }
        } else {
            scheduleBuild();
        }
886 887 888 889 890 891 892
        rsp.forwardToPreviousPage(req);
    }

    /**
     * Schedules a new SCM polling command.
     */
    public void doPolling( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
893 894 895
        BuildAuthorizationToken.checkPermission(this, authToken, req, rsp);
        schedulePolling();
        rsp.forwardToPreviousPage(req);
896 897 898 899 900 901
    }

    /**
     * Cancels a scheduled build.
     */
    public void doCancelQueue( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException {
902
        checkPermission(BUILD);
903

904
        Hudson.getInstance().getQueue().cancel(this);
905 906 907
        rsp.forwardToPreviousPage(req);
    }

908 909 910
    @Override
    protected void submit(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException, FormException {
        super.submit(req,rsp);
911 912 913 914 915 916 917 918 919 920 921 922 923 924

        disabled = req.getParameter("disable")!=null;

        jdk = req.getParameter("jdk");
        if(req.getParameter("hasCustomQuietPeriod")!=null) {
            quietPeriod = Integer.parseInt(req.getParameter("quiet_period"));
        } else {
            quietPeriod = null;
        }

        if(req.getParameter("hasSlaveAffinity")!=null) {
            canRoam = false;
            assignedNode = req.getParameter("slave");
            if(assignedNode !=null) {
925
                if(Hudson.getInstance().getLabel(assignedNode).isEmpty())
926
                    assignedNode = null;   // no such label
927 928 929 930 931 932
            }
        } else {
            canRoam = true;
            assignedNode = null;
        }

933
        authToken = BuildAuthorizationToken.create(req);
934

935 936 937 938
        setScm(SCMS.parseSCM(req));

        for (Trigger t : triggers)
            t.stop();
939
        triggers = buildDescribable(req, Triggers.getApplicableTriggers(this), "trigger");
940 941
        for (Trigger t : triggers)
            t.start(this,true);
942 943

        updateTransientActions();
944 945
    }

946
    protected final <T extends Describable<T>> List<T> buildDescribable(StaplerRequest req, List<? extends Descriptor<T>> descriptors, String prefix)
K
kohsuke 已提交
947
        throws FormException {
948

949
        JSONObject data = StructuredForm.get(req);
950
        List<T> r = new Vector<T>();
951
        for( int i=0; i< descriptors.size(); i++ ) {
952 953 954
            String name = prefix + i;
            if(req.getParameter(name)!=null) {
                T instance = descriptors.get(i).newInstance(req,data.getJSONObject(name));
955
                r.add(instance);
956 957
            }
        }
958
        return r;
959 960 961 962 963 964
    }

    /**
     * Serves the workspace files.
     */
    public void doWs( StaplerRequest req, StaplerResponse rsp ) throws IOException, ServletException, InterruptedException {
965
        checkPermission(AbstractProject.WORKSPACE);
966
        FilePath ws = getWorkspace();
967
        if ((ws == null) || (!ws.exists())) {
968
            // if there's no workspace, report a nice error message
969
            req.getView(this,"noWorkspace.jelly").forward(req,rsp);
970
        } else {
K
kohsuke 已提交
971
            new DirectoryBrowserSupport(this,getDisplayName()+" workspace").serveFile(req, rsp, ws, "folder.gif", true);
972 973
        }
    }
974

K
kohsuke 已提交
975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001
    /**
     * RSS feed for changes in this project.
     */
    public void doRssChangelog(  StaplerRequest req, StaplerResponse rsp  ) throws IOException, ServletException {
        class FeedItem {
            ChangeLogSet.Entry e;
            int idx;

            public FeedItem(Entry e, int idx) {
                this.e = e;
                this.idx = idx;
            }

            AbstractBuild<?,?> getBuild() {
                return e.getParent().build;
            }
        }

        List<FeedItem> entries = new ArrayList<FeedItem>();

        for(R r=getLastBuild(); r!=null; r=r.getPreviousBuild()) {
            int idx=0;
            for( ChangeLogSet.Entry e : r.getChangeSet())
                entries.add(new FeedItem(e,idx++));
        }

        RSS.forwardToRss(
1002
            getDisplayName()+' '+getScm().getDescriptor().getDisplayName()+" changes",
K
kohsuke 已提交
1003 1004 1005
            getUrl()+"changes",
            entries, new FeedAdapter<FeedItem>() {
                public String getEntryTitle(FeedItem item) {
K
kohsuke 已提交
1006
                    return "#"+item.getBuild().number+' '+item.e.getMsg()+" ("+item.e.getAuthor()+")";
K
kohsuke 已提交
1007 1008 1009 1010 1011 1012 1013 1014 1015 1016
                }

                public String getEntryUrl(FeedItem item) {
                    return item.getBuild().getUrl()+"changes#detail"+item.idx;
                }

                public String getEntryID(FeedItem item) {
                    return getEntryUrl(item);
                }

1017 1018 1019 1020 1021 1022 1023
                public String getEntryDescription(FeedItem item) {
                    StringBuilder buf = new StringBuilder();
                    for(String path : item.e.getAffectedPaths())
                        buf.append(path).append('\n');
                    return buf.toString();
                }

K
kohsuke 已提交
1024 1025 1026 1027 1028 1029 1030
                public Calendar getEntryTimestamp(FeedItem item) {
                    return item.getBuild().getTimestamp();
                }
            },
            req, rsp );
    }

1031 1032 1033 1034
    /**
     * Finds a {@link AbstractProject} that has the name closest to the given name.
     */
    public static AbstractProject findNearest(String name) {
1035
        List<AbstractProject> projects = Hudson.getInstance().getItems(AbstractProject.class);
1036 1037 1038 1039 1040 1041 1042
        String[] names = new String[projects.size()];
        for( int i=0; i<projects.size(); i++ )
            names[i] = projects.get(i).getName();

        String nearest = EditDistance.findNearest(name, names);
        return (AbstractProject)Hudson.getInstance().getItem(nearest);
    }
1043

H
huybrechts 已提交
1044 1045 1046 1047 1048 1049 1050 1051 1052
    /**
     * Returns the {@link ACL} for this object.
     * We need to override the identical method in AbstractItem because we won't
     * call getACL(AbstractProject) otherwise (single dispatch)
     */
    public ACL getACL() {
        return Hudson.getInstance().getAuthorizationStrategy().getACL(this);
    }

1053 1054 1055 1056 1057
    private static final Comparator<Integer> REVERSE_INTEGER_COMPARATOR = new Comparator<Integer>() {
        public int compare(Integer o1, Integer o2) {
            return o2-o1;
        }
    };
1058 1059

    private static final Logger LOGGER = Logger.getLogger(AbstractProject.class.getName());
1060

K
kohsuke 已提交
1061
    public static final Permission BUILD = new Permission(PERMISSIONS, "Build", Permission.UPDATE);
1062
    public static final Permission WORKSPACE = new Permission(PERMISSIONS, "Workspace", Permission.READ);
1063 1064 1065 1066
    /**
     * Permission to abort a build. For now, let's make it the same as {@link #BUILD}
     */
    public static final Permission ABORT = BUILD;
1067
}
H
huybrechts 已提交
1068