Java tutorial
/* * 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; } }