com.cloudbees.hudson.plugins.folder.computed.ComputedFolder.java Source code

Java tutorial

Introduction

Here is the source code for com.cloudbees.hudson.plugins.folder.computed.ComputedFolder.java

Source

/*
 * The MIT License
 *
 * Copyright 2015 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 com.cloudbees.hudson.plugins.folder.computed;

import com.cloudbees.hudson.plugins.folder.AbstractFolder;
import com.thoughtworks.xstream.XStreamException;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.AbortException;
import hudson.ExtensionList;
import hudson.Util;
import hudson.XmlFile;
import hudson.model.Action;
import hudson.model.BuildableItem;
import hudson.model.Cause;
import hudson.model.CauseAction;
import hudson.model.Descriptor;
import hudson.model.Executor;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.Items;
import hudson.model.Label;
import hudson.model.Node;
import hudson.model.Queue;
import hudson.model.ResourceList;
import hudson.model.Result;
import hudson.model.TaskListener;
import hudson.model.TopLevelItem;
import hudson.model.User;
import hudson.model.listeners.ItemListener;
import hudson.model.queue.CauseOfBlockage;
import hudson.model.queue.SubTask;
import hudson.security.ACL;
import hudson.triggers.TimerTrigger;
import hudson.triggers.Trigger;
import hudson.triggers.TriggerDescriptor;
import hudson.util.DescribableList;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.servlet.ServletException;
import jenkins.model.CauseOfInterruption;
import jenkins.model.Jenkins;
import jenkins.model.ParameterizedJobMixIn;
import jenkins.util.TimeDuration;
import net.jcip.annotations.GuardedBy;
import net.sf.json.JSONObject;
import org.acegisecurity.Authentication;
import org.apache.commons.io.FileUtils;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.DoNotUse;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpResponses;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.interceptor.RequirePOST;

import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;

/**
 * A folder-like item whose children are computed.
 * Users cannot directly add or remove (or rename) children.
 * The children should also not offer {@link Item#CONFIGURE} to anyone.
 * @param <I> the child item type
 * @since 4.11-beta-1
 */
@SuppressWarnings({ "unchecked", "rawtypes", "deprecation" }) // generics mistakes in various places; BuildableItem defines deprecated methods (and @SW on those overrides does not seem to work)
public abstract class ComputedFolder<I extends TopLevelItem> extends AbstractFolder<I>
        implements BuildableItem, Queue.FlyweightTask {

    /**
     * Our logger.
     */
    private static final Logger LOGGER = Logger.getLogger(ComputedFolder.class.getName());

    /**
     * Our {@link OrphanedItemStrategy}
     */
    private OrphanedItemStrategy orphanedItemStrategy;

    /**
     * Our {@link Trigger}s.
     */
    private DescribableList<Trigger<?>, TriggerDescriptor> triggers;

    /**
     * Our {@link FolderComputation}.
     */
    @Nonnull
    private transient FolderComputation<I> computation;

    /**
     * Lock to guard {@link #currentObservations}.
     *
     * @since 6.0.0
     */
    @Nonnull
    private transient /* almost final */ ReentrantLock currentObservationsLock;
    /**
     * Condition to flag whenever the {@link #currentObservationsChanged} has had elements removed.
     *
     * @since 6.0.0
     */
    private transient /* almost final */ Condition currentObservationsChanged;
    /**
     * The names of the child items that are currently being observed.
     *
     * @since 6.0.0
     */
    @GuardedBy("#computationLock")
    private transient /* almost final */ Set<String> currentObservations;
    /**
     * Flag set when the implementation uses {@link #createEventsChildObserver()} and not
     * {@link #openEventsChildObserver()}, when {@code true} then the {@link #currentObservations} is ignored
     * as we cannot rely on the implementation to call {@link ChildObserver#close()}.
     *
     * @since 6.0.0
     */
    @GuardedBy("#computationLock")
    private transient boolean currentObservationsLockDisabled;

    /**
     * Tracks recalculation requirements in {@link #doConfigSubmit(StaplerRequest, StaplerResponse)}.
     *
     * @see #recalculateAfterSubmitted(boolean)
     * @see #submit(StaplerRequest, StaplerResponse)
     * @see #doConfigSubmit(StaplerRequest, StaplerResponse)
     * @since FIXME
     */
    private transient Recalculation recalculate;

    @SuppressFBWarnings(value = "NP_NONNULL_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR")
    protected ComputedFolder(ItemGroup parent, String name) {
        super(parent, name);
        init();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected final void init() {
        super.init();
        if (orphanedItemStrategy == null) {
            orphanedItemStrategy = new DefaultOrphanedItemStrategy(true, "0", "0");
        }
        if (triggers == null) {
            triggers = new DescribableList<Trigger<?>, TriggerDescriptor>(this);
        } else {
            triggers.setOwner(this);
        }
        for (Trigger t : triggers) {
            t.start(this, Items.currentlyUpdatingByXml());
        }
        synchronized (this) {
            computation = createComputation(null);
        }
        currentObservationsLock = new ReentrantLock();
        currentObservationsChanged = currentObservationsLock.newCondition();
        currentObservations = new HashSet<>();
    }

    @Override
    public void onCreatedFromScratch() {
        try {
            FileUtils.forceMkdir(getComputationDir());
        } catch (IOException x) {
            LOGGER.log(Level.WARNING, null, x);
        }
        super.onCreatedFromScratch();
    }

    @Override
    public void onLoad(ItemGroup<? extends Item> parent, String name) throws IOException {
        super.onLoad(parent, name);
        try {
            FileUtils.forceMkdir(getComputationDir());
        } catch (IOException x) {
            LOGGER.log(Level.WARNING, null, x);
        }
        synchronized (this) {
            XmlFile file = computation.getDataFile();
            if (file.exists()) {
                try {
                    file.unmarshal(computation);
                } catch (IOException e) {
                    LOGGER.log(Level.WARNING, "Failed to load " + file, e);
                }
            }
        }
    }

    /**
     * Called to (re-)compute the set of children of this folder. It is recommended that the computation checks the
     * {@link Thread#interrupted()} status and throws a {@link InterruptedException} if set at least once every 5
     * seconds to allow the user to interrupt a computation..
     *
     * @param observer how to indicate which children should be seen
     * @param listener a way to report progress
     * @throws IOException if there was an {@link IOException} during the computation.
     * @throws InterruptedException if the computation was interrupted.
     */
    protected abstract void computeChildren(ChildObserver<I> observer, TaskListener listener)
            throws IOException, InterruptedException;

    /**
     * Hook called when some items are no longer in the list.
     * Do not call {@link Item#delete} or {@link ItemGroup#onDeleted} or {@link ItemListener#fireOnDeleted} yourself.
     * By default, uses {@link #getOrphanedItemStrategy}.
     * @param orphaned a nonempty set of items which no longer are supposed to be here
     * @return any subset of {@code orphaned}, representing those children which ought to be removed from the folder now; items not listed will be left alone for the time being
     */
    protected Collection<I> orphanedItems(Collection<I> orphaned, TaskListener listener)
            throws IOException, InterruptedException {
        return getOrphanedItemStrategy().orphanedItems(this, orphaned, listener);
    }

    public void setOrphanedItemStrategy(@Nonnull OrphanedItemStrategy strategy) {
        this.orphanedItemStrategy = strategy;
    }

    /**
     * Recreates children synchronously.
     */
    final void updateChildren(final TaskListener listener) throws IOException, InterruptedException {
        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.log(Level.FINE, "updating {0}", getFullName());
        }
        try (FullReindexChildObserver observer = new FullReindexChildObserver()) {
            computeChildren(observer, listener);
            Map<String, I> orphaned = observer.orphaned();
            if (!orphaned.isEmpty()) {
                if (LOGGER.isLoggable(Level.FINE)) {
                    LOGGER.log(Level.FINE, "{0}: orphaned {1}", new Object[] { getFullName(), orphaned.keySet() });
                }
                for (I existing : orphanedItems(orphaned.values(), listener)) {
                    if (LOGGER.isLoggable(Level.FINE)) {
                        LOGGER.log(Level.FINE, "{0}: deleting {1}", new Object[] { getFullName(), existing });
                    }
                    existing.delete();
                    // super.onDeleted handles removal from items
                }
            }
        }
        if (LOGGER.isLoggable(Level.FINE)) {
            LOGGER.log(Level.FINE, "finished updating {0}", getFullName());
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void delete() throws IOException, InterruptedException {
        checkPermission(DELETE);
        FolderComputation<I> computation = getComputation();
        Executor executor = Executor.of(computation);
        if (executor != null) {
            LOGGER.log(Level.INFO, "Interrupting {0} in order to delete it", this);
            executor.interrupt(Result.ABORTED, new CauseOfInterruption.UserInterruption(User.current()));
            // give it 15 seconds or so to respond to the interrupt
            long expiration = System.nanoTime() + TimeUnit.SECONDS.toNanos(15);
            // comparison with executor.getCurrentExecutable() == computation currently should always be true
            // as we no longer recycle Executors, but safer to future-proof in case we ever revisit recycling
            while (executor.isAlive() && executor.getCurrentExecutable() == computation
                    && expiration - System.nanoTime() > 0L) {
                Thread.sleep(50L);
            }
            if (executor.isAlive() && executor.getCurrentExecutable() == computation) {
                throw new AbortException("Failed to stop computation of " + getFullDisplayName());
            }
        }
        super.delete();
    }

    /**
     * Creates a {@link ChildObserver} that subclasses can use when handling events that might create new / update
     * existing child items. The handling of orphaned items is a responsibility of the {@link OrphanedItemStrategy}
     * which is only applied as part of a full computation.
     *
     * @return a {@link ChildObserver} for event handling.
     * @deprecated use {@link #openEventsChildObserver()}
     */
    @Deprecated
    @Restricted(NoExternalUse.class) // cause a compilation error to force implementations to switch
    protected final ChildObserver<I> createEventsChildObserver() {
        LOGGER.log(Level.WARNING, "The {0} implementation of ComputedFolder has not been updated to use "
                + "openEventsChildObserver(), this may result in 'java.lang.IllegalStateException: JENKINS-23152 ... "
                + "already existed; will not overwrite with ...' being thrown when processing events",
                getClass().getName());
        currentObservationsLock.lock();
        try {
            if (!currentObservationsLockDisabled) {
                currentObservationsLockDisabled = true;
                currentObservationsChanged.signalAll();
            }
        } finally {
            currentObservationsLock.unlock();
        }
        return new EventChildObserver();
    }

    /**
     * Opens a new {@link ChildObserver} that subclasses can use when handling events that might create new / update
     * existing child items. The handling of orphaned items is a responsibility of the {@link OrphanedItemStrategy}
     * which is only applied as part of a full computation.
     *
     * @return a {@link ChildObserver} for event handling. The caller must {@link ChildObserver#close()} when done.
     * @since 6.0.0
     */
    protected final ChildObserver<I> openEventsChildObserver() {
        return new EventChildObserver();
    }

    /**
     * {@inheritDoc}
     */
    @RequirePOST
    @Override
    public void doConfigSubmit(StaplerRequest req, StaplerResponse rsp)
            throws IOException, ServletException, Descriptor.FormException {
        try {
            recalculate = Recalculation.UNKNOWN;
            super.doConfigSubmit(req, rsp);
            if (recalculate != Recalculation.NO_RECALCULATION && isBuildable()) {
                scheduleBuild();
            }
        } finally {
            recalculate = null;
        }
    }

    /**
     * Method for child classes to use if they want to suppress/confirm the automatic recalculation provided in
     * {@link #doConfigSubmit(StaplerRequest, StaplerResponse)}. This method should only be called from
     * {@link #submit(StaplerRequest, StaplerResponse)}. If called multiple times from
     * {@link #submit(StaplerRequest, StaplerResponse)} then all calls must be with the {@code false} parameter
     * to suppress recalculation.
     *
     * @param recalculate {@code true} to require recalculation, {@code false} to suppress recalculation.
     * @since FIXME
     * @see #submit(StaplerRequest, StaplerResponse)
     */
    /*
     * Note: it would have been much nicer to have submit(req,rsp) return a boolean... but that would have required
     * doConfigSubmit(req,rsp) to also return a boolean which would have required the @WebMethod annotation to get
     * Stapler to detect the method as a web method...
     */
    protected final void recalculateAfterSubmitted(boolean recalculate) {
        if (this.recalculate == null) {
            return;
        }
        if (this.recalculate == Recalculation.RECALCULATE) {
            return;
        }
        this.recalculate = recalculate ? Recalculation.RECALCULATE : Recalculation.NO_RECALCULATION;
    }

    /**
     * {@inheritDoc}
     *
     * @see #recalculateAfterSubmitted(boolean)
     */
    @Override
    protected void submit(StaplerRequest req, StaplerResponse rsp)
            throws IOException, ServletException, Descriptor.FormException {
        String oisDigest = null;
        try {
            oisDigest = Util.getDigestOf(Items.XSTREAM2.toXML(orphanedItemStrategy));
        } catch (XStreamException e) {
            // ignore
        }
        super.submit(req, rsp);
        JSONObject json = req.getSubmittedForm();
        orphanedItemStrategy = req.bindJSON(OrphanedItemStrategy.class, json.getJSONObject("orphanedItemStrategy"));
        for (Trigger t : triggers) {
            t.stop();
        }
        triggers.rebuild(req, json, Trigger.for_(this));
        for (Trigger t : triggers) {
            t.start(this, true);
        }
        try {
            if (oisDigest == null
                    || !oisDigest.equals(Util.getDigestOf(Items.XSTREAM2.toXML(orphanedItemStrategy)))) {
                // force a recalculation if orphanedItemStrategy has changed as recalculation is when we find orphans
                recalculateAfterSubmitted(true);
            }
        } catch (XStreamException e) {
            // force a recalculation anyway in this case
            recalculateAfterSubmitted(true);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Restricted(NoExternalUse.class)
    @Override
    @Nonnull
    protected final String getSuccessfulDestination() {
        if (recalculate != Recalculation.NO_RECALCULATION && isBuildable()) {
            return computation.getSearchUrl() + "console";
        } else {
            return super.getSuccessfulDestination();
        }
    }

    public Map<TriggerDescriptor, Trigger<?>> getTriggers() {
        return triggers.toMap();
    }

    @Restricted(NoExternalUse.class) // currently used only by jelly / stapler
    public List<TriggerDescriptor> getTriggerDescriptors() {
        // TODO remove this once core has support for DescriptorVisibilityFilter on Trigger.for_(Item)
        List<TriggerDescriptor> result = new ArrayList<TriggerDescriptor>();
        for (TriggerDescriptor d : Trigger.for_(this)) {
            if (d instanceof TimerTrigger.DescriptorImpl) {
                continue;
            }
            result.add(d);
        }
        return result;
    }

    public void addTrigger(Trigger trigger) {
        Trigger old = triggers.get(trigger.getDescriptor());
        if (old != null) {
            old.stop();
            triggers.remove(old);
        }
        triggers.add(trigger);
        trigger.start(this, true);
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("deprecation")
    @Override
    public List<Action> getActions() {
        List<Action> actions = new ArrayList<Action>(super.getActions());
        for (Trigger<?> trigger : triggers) {
            actions.addAll(trigger.getProjectActions());
        }
        return actions;
    }

    /** Whether it is permissible to recompute this folder at this time. */
    public boolean isBuildable() {
        return true;
    }

    @RequirePOST
    public HttpResponse doBuild(@QueryParameter TimeDuration delay) {
        checkPermission(BUILD);
        if (!isBuildable()) {
            throw HttpResponses.error(SC_INTERNAL_SERVER_ERROR,
                    new IOException(getFullName() + " cannot be recomputed"));
        }
        scheduleBuild2(delay == null ? 0 : delay.getTime(), new CauseAction(new Cause.UserIdCause()));
        return HttpResponses.forwardToPreviousPage();
    }

    /** Duck-types {@link ParameterizedJobMixIn#scheduleBuild2(int, Action...)}. */
    @CheckForNull
    public Queue.Item scheduleBuild2(int quietPeriod, Action... actions) {
        if (!isBuildable()) {
            return null;
        }
        Jenkins j = Jenkins.getInstance();
        if (j == null) {
            return null;
        }
        return j.getQueue().schedule2(this, quietPeriod, Arrays.asList(actions)).getItem();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean scheduleBuild() {
        return scheduleBuild2(0) != null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean scheduleBuild(Cause c) {
        return scheduleBuild2(0, new CauseAction(c)) != null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean scheduleBuild(int quietPeriod) {
        return scheduleBuild2(quietPeriod) != null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean scheduleBuild(int quietPeriod, Cause c) {
        return scheduleBuild2(quietPeriod, new CauseAction(c)) != null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isBuildBlocked() {
        return getCauseOfBlockage() != null;
    }

    /**
     * {@inheritDoc}
     */
    @Deprecated
    @Override
    public String getWhyBlocked() {
        CauseOfBlockage causeOfBlockage = getCauseOfBlockage();
        return causeOfBlockage == null ? null : causeOfBlockage.getShortDescription();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public CauseOfBlockage getCauseOfBlockage() {
        if (computation.isBuilding()) {
            return CauseOfBlockage.fromMessage(Messages._ComputedFolder_already_computing());
        }
        return null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void checkAbortPermission() {
        checkPermission(CANCEL);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean hasAbortPermission() {
        return hasPermission(CANCEL);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isConcurrentBuild() {
        return false;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Collection<? extends SubTask> getSubTasks() {
        return Collections.singleton(this);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Authentication getDefaultAuthentication() {
        return ACL.SYSTEM;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Authentication getDefaultAuthentication(Queue.Item item) {
        return getDefaultAuthentication();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Label getAssignedLabel() {
        Jenkins j = Jenkins.getInstance();
        if (j == null) {
            return null;
        }
        return j.getSelfLabel();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Node getLastBuiltOn() {
        return Jenkins.getInstance();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public long getEstimatedDuration() {
        return computation.getEstimatedDuration();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public final FolderComputation<I> createExecutable() throws IOException {
        FolderComputation<I> c = createComputation(computation);
        computation = c;
        LOGGER.log(Level.FINE, "Recording {0} @{1}", new Object[] { c, c.getTimestamp() });
        return c;
    }

    @Nonnull
    protected FolderComputation<I> createComputation(@CheckForNull FolderComputation<I> previous) {
        return new FolderComputation<I>(this, previous);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Queue.Task getOwnerTask() {
        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object getSameNodeConstraint() {
        return null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ResourceList getResourceList() {
        return ResourceList.EMPTY;
    }

    protected File getComputationDir() {
        return new File(getRootDir(), "computation");
    }

    /**
     * Identifies if this {@link ComputedFolder} has a separate out of band events log. Default implementation
     * just checks if the events log has content. Subclasses can override this method to force the events log
     * always present in the UI.
     *
     * @return {@code true} if this {@link ComputedFolder} has a separate out of band events log.
     * @since FIXME
     */
    public boolean isHasEvents() {
        return getComputation().getEventsFile().length() > 0;
    }

    /**
     * URL binding and other purposes.
     * It may be null temporarily inside the constructor, so beware if you extend this class.
     */
    @Nonnull
    public FolderComputation<I> getComputation() {
        return computation;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected String renameBlocker() {
        if (computation.isBuilding()) {
            return "Recomputation is currently in progress";
        }
        return super.renameBlocker();
    }

    @NonNull
    public OrphanedItemStrategy getOrphanedItemStrategy() {
        return orphanedItemStrategy;
    }

    /**
     * Gets the {@link OrphanedItemStrategyDescriptor}s applicable to this folder.
     */
    @Restricted(DoNotUse.class) // used by Jelly
    @NonNull
    public List<OrphanedItemStrategyDescriptor> getOrphanedItemStrategyDescriptors() {
        List<OrphanedItemStrategyDescriptor> result = new ArrayList<OrphanedItemStrategyDescriptor>();
        for (OrphanedItemStrategyDescriptor descriptor : ExtensionList
                .lookup(OrphanedItemStrategyDescriptor.class)) {
            if (descriptor.isApplicable(getClass())) {
                result.add(descriptor);
            }
        }
        return result;
    }

    private class FullReindexChildObserver extends ChildObserver<I> {
        private final Map<String, I> orphaned = new HashMap<String, I>(items);
        private final Set<String> observed = new HashSet<String>();
        private final Set<String> locked = new HashSet<String>();
        private final String fullName = getFullName();

        @Override
        public void close() {
            if (!locked.isEmpty()) {
                currentObservationsLock.lock();
                try {
                    currentObservations.removeAll(locked);
                    currentObservationsChanged.signalAll();
                } finally {
                    currentObservationsLock.unlock();
                }
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public I shouldUpdate(String name) throws InterruptedException {
            currentObservationsLock.lock();
            try {
                while (!currentObservations.add(name) && !currentObservationsLockDisabled) {
                    currentObservationsChanged.await();
                }
                locked.add(name);
            } finally {
                currentObservationsLock.unlock();
            }
            I existing = orphaned.remove(name);
            if (existing == null) {
                // may have been created by a parallel event
                existing = items.get(name);
            }
            if (existing != null) {
                observed.add(name);
            }
            LOGGER.log(Level.FINE, "{0}: existing {1}: {2}", new Object[] { fullName, name, existing });
            return existing;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public boolean mayCreate(String name) {
            boolean r = observed.add(name);
            LOGGER.log(Level.FINE, "{0}: may create {1}? {2}", new Object[] { fullName, name, r });
            if (!r) {
                completed(name);
            }
            return r;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void created(I child) {
            if (child.getParent() != ComputedFolder.this) {
                throw new IllegalStateException("Tried to notify " + ComputedFolder.this + " of creation of "
                        + child + " with a different parent");
            }
            LOGGER.log(Level.FINE, "{0}: created {1}", new Object[] { fullName, child });
            String name = child.getName();
            if (!observed.contains(name)) {
                throw new IllegalStateException("Did not call mayCreate, or used the wrong Item.name for " + child
                        + " with name " + name + " among " + observed);
            }
            child.onCreatedFromScratch();
            try {
                child.save();
            } catch (IOException x) {
                LOGGER.log(Level.WARNING, "Failed to save " + child, x);
            }
            itemsPut(name, child);
            Jenkins j = Jenkins.getInstance();
            if (j != null) {
                j.rebuildDependencyGraphAsync();
            }
            ItemListener.fireOnCreated(child);
            // signal this name early
            completed(name);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void completed(String name) {
            if (locked.contains(name)) {
                currentObservationsLock.lock();
                try {
                    locked.remove(name);
                    currentObservations.remove(name);
                    currentObservationsChanged.signalAll();
                } finally {
                    currentObservationsLock.unlock();
                }
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Set<String> observed() {
            return new HashSet<String>(observed);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Map<String, I> orphaned() {
            return new HashMap<String, I>(orphaned);
        }
    }

    private class EventChildObserver extends ChildObserver<I> {
        private final String fullName = getFullName();
        private final Set<String> observed = new HashSet<String>();
        private final Set<String> locked = new HashSet<String>();

        @Override
        public void close() {
            if (!locked.isEmpty()) {
                currentObservationsLock.lock();
                try {
                    currentObservations.removeAll(locked);
                    currentObservationsChanged.signalAll();
                } finally {
                    currentObservationsLock.unlock();
                }
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public I shouldUpdate(String name) throws InterruptedException {
            currentObservationsLock.lock();
            try {
                while (!currentObservations.add(name) && !currentObservationsLockDisabled) {
                    currentObservationsChanged.await();
                }
                locked.add(name);
            } finally {
                currentObservationsLock.unlock();
            }
            I existing = items.get(name);
            if (existing != null) {
                observed.add(name);
            }
            LOGGER.log(Level.FINE, "{0}: existing {1}: {2}", new Object[] { fullName, name, existing });
            return existing;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public boolean mayCreate(String name) {
            boolean r = !items.containsKey(name) && observed.add(name);
            LOGGER.log(Level.FINE, "{0}: may create {1}? {2}", new Object[] { fullName, name, r });
            if (!r) {
                completed(name);
            }
            return r;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void created(I child) {
            if (child.getParent() != ComputedFolder.this) {
                throw new IllegalStateException("Tried to notify " + ComputedFolder.this + " of creation of "
                        + child + " with a different parent");
            }
            LOGGER.log(Level.FINE, "{0}: created {1}", new Object[] { fullName, child });
            String name = child.getName();
            if (!observed.contains(name)) {
                throw new IllegalStateException("Did not call mayCreate, or used the wrong Item.name for " + child
                        + " with name " + name + " among " + observed);
            }
            child.onCreatedFromScratch();
            try {
                child.save();
            } catch (IOException x) {
                LOGGER.log(Level.WARNING, "Failed to save " + child, x);
            }
            itemsPut(name, child);
            Jenkins j = Jenkins.getInstance();
            if (j != null) {
                j.rebuildDependencyGraphAsync();
            }
            ItemListener.fireOnCreated(child);
            // signal this name early
            completed(name);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void completed(String name) {
            if (locked.contains(name)) {
                currentObservationsLock.lock();
                try {
                    locked.remove(name);
                    currentObservations.remove(name);
                    currentObservationsChanged.signalAll();
                } finally {
                    currentObservationsLock.unlock();
                }
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Set<String> observed() {
            return new HashSet<String>(observed);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public Map<String, I> orphaned() {
            return new HashMap<String, I>(); // always empty as we never orphan items from events
        }
    }

    /**
     * Records the recalculation requirements of a call to {@link #submit(StaplerRequest, StaplerResponse)}.
     */
    private enum Recalculation {
        /**
         * We don't know if recalculation is required... assume it will be.
         */
        UNKNOWN,
        /**
         * We know recalculation is required.
         */
        RECALCULATE,
        /**
         * We know recalculation is not required.
         */
        NO_RECALCULATION;
    }
}