jenkins.branch.OrganizationFolder.java Source code

Java tutorial

Introduction

Here is the source code for jenkins.branch.OrganizationFolder.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 jenkins.branch;

import antlr.ANTLRException;
import com.cloudbees.hudson.plugins.folder.AbstractFolderDescriptor;
import com.cloudbees.hudson.plugins.folder.ChildNameGenerator;
import com.cloudbees.hudson.plugins.folder.FolderIcon;
import com.cloudbees.hudson.plugins.folder.FolderIconDescriptor;
import com.cloudbees.hudson.plugins.folder.computed.ChildObserver;
import com.cloudbees.hudson.plugins.folder.computed.ComputedFolder;
import com.cloudbees.hudson.plugins.folder.computed.EventOutputStreams;
import com.cloudbees.hudson.plugins.folder.computed.FolderComputation;
import com.cloudbees.hudson.plugins.folder.computed.PeriodicFolderTrigger;
import com.cloudbees.hudson.plugins.folder.views.AbstractFolderViewHolder;
import com.thoughtworks.xstream.XStreamException;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.BulkChange;
import hudson.Extension;
import hudson.ExtensionList;
import hudson.Util;
import hudson.XmlFile;
import hudson.console.ModelHyperlinkNote;
import hudson.init.InitMilestone;
import hudson.model.Action;
import hudson.model.Cause;
import hudson.model.Descriptor;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.Items;
import hudson.model.Saveable;
import hudson.model.StreamBuildListener;
import hudson.model.TaskListener;
import hudson.model.TopLevelItem;
import hudson.model.View;
import hudson.model.listeners.SaveableListener;
import hudson.util.DescribableList;
import hudson.util.StreamTaskListener;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
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.Jenkins;
import jenkins.model.TransientActionFactory;
import jenkins.scm.api.SCMEvent;
import jenkins.scm.api.SCMEventListener;
import jenkins.scm.api.SCMHeadEvent;
import jenkins.scm.api.SCMNavigator;
import jenkins.scm.api.SCMNavigatorDescriptor;
import jenkins.scm.api.SCMNavigatorEvent;
import jenkins.scm.api.SCMNavigatorOwner;
import jenkins.scm.api.SCMSource;
import jenkins.scm.api.SCMSourceCategory;
import jenkins.scm.api.SCMSourceCriteria;
import jenkins.scm.api.SCMSourceEvent;
import jenkins.scm.api.SCMSourceObserver;
import jenkins.scm.api.SCMSourceOwner;
import jenkins.scm.api.metadata.ObjectMetadataAction;
import jenkins.scm.impl.SingleSCMNavigator;
import jenkins.scm.impl.UncategorizedSCMSourceCategory;
import org.acegisecurity.AccessDeniedException;
import org.apache.commons.io.Charsets;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
import org.jenkins.ui.icon.Icon;
import org.jenkins.ui.icon.IconSet;
import org.jenkins.ui.icon.IconSpec;
import org.jvnet.localizer.LocaleProvider;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;

import static jenkins.scm.api.SCMEvent.Type.CREATED;
import static jenkins.scm.api.SCMEvent.Type.UPDATED;

/**
 * A folder-like collection of {@link MultiBranchProject}s, one per repository.
 */
@Restricted(NoExternalUse.class) // not currently intended as an API
@SuppressWarnings({ "unchecked", "rawtypes" }) // mistakes in various places
public final class OrganizationFolder extends ComputedFolder<MultiBranchProject<?, ?>>
        implements SCMNavigatorOwner, IconSpec {

    /**
     * Our logger.
     */
    private static final Logger LOGGER = Logger.getLogger(MultiBranchProject.class.getName());
    /**
     * Our navigators.
     */
    private final DescribableList<SCMNavigator, SCMNavigatorDescriptor> navigators = new DescribableList<SCMNavigator, SCMNavigatorDescriptor>(
            this);
    /**
     * Our project factories.
     */
    private final DescribableList<MultiBranchProjectFactory, MultiBranchProjectFactoryDescriptor> projectFactories = new DescribableList<MultiBranchProjectFactory, MultiBranchProjectFactoryDescriptor>(
            this);

    /**
     * The persisted state maintained outside of the config file.
     *
     * @since 2.0
     */
    private transient /*almost final*/ State state = new State(this);

    /**
     * The navigator digest used to detect if we need to trigger a rescan on save.
     *
     * @since 2.0
     */
    private transient String navDigest;

    /**
     * The factory digest used to detect if we need to trigger a rescan on save.
     *
     * @since 2.0
     */
    private transient String facDigest;

    /**
     * {@inheritDoc}
     */
    public OrganizationFolder(ItemGroup parent, String name) {
        super(parent, name);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onCreatedFromScratch() {
        super.onCreatedFromScratch();
        for (MultiBranchProjectFactoryDescriptor d : ExtensionList
                .lookup(MultiBranchProjectFactoryDescriptor.class)) {
            MultiBranchProjectFactory f = d.newInstance();
            if (f != null) {
                projectFactories.add(f);
            }
        }
        try {
            addTrigger(new PeriodicFolderTrigger("1d"));
        } catch (ANTLRException x) {
            throw new IllegalStateException(x);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onLoad(ItemGroup<? extends Item> parent, String name) throws IOException {
        super.onLoad(parent, name);
        navigators.setOwner(this);
        projectFactories.setOwner(this);
        if (!(getFolderViews() instanceof OrganizationFolderViewHolder)) {
            resetFolderViews();
        }
        if (!(getIcon() instanceof MetadataActionFolderIcon)) {
            setIcon(newDefaultFolderIcon());
        }
        if (state == null) {
            state = new State(this);
        }
        try {
            state.load();
        } catch (XStreamException | IOException e) {
            LOGGER.log(Level.WARNING, "Could not read persisted state, will be recovered on next index.", e);
            state.reset();
        }
        if (getComputation().getLogFile().isFile()) {
            // TODO find a more reliable way to detect if the folder has not been scanned since creation
            // Basically we want the first save after a config change to trigger a scan.
            // The above condition will cover the very first save, but will not cover the case of the configuration
            // being changed *by code not the user*, saved and then Jenkins restarted before the scan occurs.
            // Should not be a big deal as periodic scan will pick it up eventually and user can always manually force
            // the issue by triggering a manual scan
            try {
                navDigest = Util.getDigestOf(Items.XSTREAM2.toXML(navigators));
            } catch (XStreamException e) {
                navDigest = null;
            }
            try {
                facDigest = Util.getDigestOf(Items.XSTREAM2.toXML(projectFactories));
            } catch (XStreamException e) {
                facDigest = null;
            }
        }
    }

    @Override
    public MultiBranchProject<?, ?> getItem(String name) throws AccessDeniedException {
        if (name == null) {
            return null;
        }
        MultiBranchProject<?, ?> item = super.getItem(name);
        if (item != null) {
            return item;
        }
        if (name.indexOf('%') != -1) {
            String decoded = NameEncoder.decode(name);
            item = super.getItem(decoded);
            if (item != null) {
                return item;
            }
            // fall through for double decoded call paths // TODO is this necessary
        }
        return super.getItem(NameEncoder.encode(name));
    }

    /**
     * Returns the child job with the specified project name or {@code null} if no such child job exists.
     *
     * @param projectName the name of the project.
     * @return the child job or {@code null} if no such job exists or if the requesting user does ave permission to
     * view it.
     * @since 2.0.0
     */
    @edu.umd.cs.findbugs.annotations.CheckForNull
    public MultiBranchProject<?, ?> getItemByProjectName(@NonNull String projectName) {
        return super.getItem(NameEncoder.encode(projectName));
    }

    /**
     * Returns {@code true} if this is a single origin {@link OrganizationFolder}.
     *
     * @return {@code true} if this is a single origin {@link OrganizationFolder}.
     */
    public boolean isSingleOrigin() {
        // JENKINS-41171 we expect everything except for rare legacy instances to be single origin.
        return navigators.size() == 1;
    }

    public DescribableList<SCMNavigator, SCMNavigatorDescriptor> getNavigators() {
        return navigators;
    }

    /**
     * {@inheritDoc}
     */
    @NonNull
    @Override
    public List<SCMNavigator> getSCMNavigators() {
        return navigators;
    }

    public DescribableList<MultiBranchProjectFactory, MultiBranchProjectFactoryDescriptor> getProjectFactories() {
        return projectFactories;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void submit(StaplerRequest req, StaplerResponse rsp)
            throws IOException, ServletException, Descriptor.FormException {
        super.submit(req, rsp);
        navigators.rebuildHetero(req, req.getSubmittedForm(), ExtensionList.lookup(SCMNavigatorDescriptor.class),
                "navigators");
        projectFactories.rebuildHetero(req, req.getSubmittedForm(),
                ExtensionList.lookup(MultiBranchProjectFactoryDescriptor.class), "projectFactories");
        for (SCMNavigator n : navigators) {
            n.afterSave(this);
        }
        String navDigest;
        try {
            navDigest = Util.getDigestOf(Items.XSTREAM2.toXML(navigators));
        } catch (XStreamException e) {
            navDigest = null;
        }
        String facDigest;
        try {
            facDigest = Util.getDigestOf(Items.XSTREAM2.toXML(projectFactories));
        } catch (XStreamException e) {
            facDigest = null;
        }
        recalculateAfterSubmitted(!StringUtils.equals(navDigest, this.navDigest));
        recalculateAfterSubmitted(!StringUtils.equals(facDigest, this.facDigest));
        this.navDigest = navDigest;
        this.facDigest = facDigest;
    }

    /**
     * {@inheritDoc}
     */
    @Nonnull
    @Override
    protected FolderComputation<MultiBranchProject<?, ?>> createComputation(
            @CheckForNull FolderComputation<MultiBranchProject<?, ?>> previous) {
        return new OrganizationScan(OrganizationFolder.this, previous);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isHasEvents() {
        return true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isBuildable() {
        return super.isBuildable() && !navigators.isEmpty() && !projectFactories.isEmpty();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void computeChildren(final ChildObserver<MultiBranchProject<?, ?>> observer,
            final TaskListener listener) throws IOException, InterruptedException {
        // capture the current digests to prevent unnecessary rescan if re-saving after scan
        try {
            navDigest = Util.getDigestOf(Items.XSTREAM2.toXML(navigators));
        } catch (XStreamException e) {
            navDigest = null;
        }
        try {
            facDigest = Util.getDigestOf(Items.XSTREAM2.toXML(projectFactories));
        } catch (XStreamException e) {
            facDigest = null;
        }
        long start = System.currentTimeMillis();
        listener.getLogger().format("[%tc] Starting organization scan...%n", start);
        try {
            listener.getLogger().format("[%tc] Updating actions...%n", System.currentTimeMillis());
            Map<SCMNavigator, List<Action>> navigatorActions = new HashMap<>();
            for (SCMNavigator navigator : navigators) {
                List<Action> actions;
                try {
                    actions = navigator.fetchActions(this, null, listener);
                } catch (IOException e) {
                    MultiBranchProject.printStackTrace(e,
                            listener.error("[%tc] Could not refresh actions for navigator %s",
                                    System.currentTimeMillis(), navigator));
                    // preserve previous actions if we have some transient error fetching now (e.g. API rate limit)
                    actions = Util.fixNull(state.getActions().get(navigator));
                }
                navigatorActions.put(navigator, actions);
            }
            // update any persistent actions for the SCMNavigator
            if (!navigatorActions.equals(state.getActions())) {
                boolean saveProject = false;
                for (List<Action> actions : navigatorActions.values()) {
                    for (Action a : actions) {
                        // undo any hacks that attached the contributed actions without attribution
                        saveProject = removeActions(a.getClass()) || saveProject;
                    }
                }
                BulkChange bc = new BulkChange(state);
                try {
                    state.setActions(navigatorActions);
                    try {
                        bc.commit();
                    } catch (IOException | RuntimeException e) {
                        MultiBranchProject.printStackTrace(e, listener
                                .error("[%tc] Could not persist folder level actions", System.currentTimeMillis()));
                        throw e;
                    }
                    if (saveProject) {
                        try {
                            save();
                        } catch (IOException | RuntimeException e) {
                            MultiBranchProject.printStackTrace(e,
                                    listener.error("[%tc] Could not persist folder level configuration changes",
                                            System.currentTimeMillis()));
                            throw e;
                        }
                    }
                } finally {
                    bc.abort();
                }
            }
            for (SCMNavigator navigator : navigators) {
                if (Thread.interrupted()) {
                    throw new InterruptedException();
                }
                listener.getLogger().format("[%tc] Consulting %s%n", System.currentTimeMillis(),
                        navigator.getDescriptor().getDisplayName());
                try {
                    navigator.visitSources(
                            new SCMSourceObserverImpl(listener, observer, navigator, (SCMSourceEvent<?>) null));
                } catch (IOException | InterruptedException | RuntimeException e) {
                    MultiBranchProject.printStackTrace(e,
                            listener.error("[%tc] Could not fetch sources from navigator %s",
                                    System.currentTimeMillis(), navigator));
                    throw e;
                }
            }
        } finally {
            long end = System.currentTimeMillis();
            listener.getLogger().format("[%tc] Finished organization scan. Scan took %s%n", end,
                    Util.getTimeSpanString(end - start));

        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected AbstractFolderViewHolder newFolderViewHolder() {
        return new OrganizationFolderViewHolder(this);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected FolderIcon newDefaultFolderIcon() {
        return new MetadataActionFolderIcon();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getIconClassName() {
        String result;
        if (navigators.size() == 1) {
            result = navigators.get(0).getDescriptor().getIconClassName();
        } else {
            result = null;
            for (int i = 0; i < navigators.size(); i++) {
                String iconClassName = navigators.get(i).getDescriptor().getIconClassName();
                if (i == 0) {
                    result = iconClassName;
                } else if (!StringUtils.equals(result, iconClassName)) {
                    result = null;
                    break;
                }
            }
        }

        return result != null ? result : getDescriptor().getIconClassName();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getPronoun() {
        Set<String> result = new TreeSet<>();
        for (SCMNavigator navigator : navigators) {
            String pronoun = Util.fixEmptyAndTrim(navigator.getPronoun());
            if (pronoun != null) {
                result.add(pronoun);
            }
        }
        return result.isEmpty() ? super.getPronoun() : StringUtils.join(result, " / ");
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<SCMSource> getSCMSources() {
        // Probably unused unless onSCMSourceUpdated implemented, but just in case:
        Set<SCMSource> result = new HashSet<SCMSource>();
        for (MultiBranchProject<?, ?> child : getItems()) {
            result.addAll(child.getSCMSources());
        }
        return new ArrayList<SCMSource>(result);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public SCMSource getSCMSource(String sourceId) {
        return null;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onSCMSourceUpdated(SCMSource source) {
        // TODO possibly we should recheck whether this project remains valid
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public SCMSourceCriteria getSCMSourceCriteria(SCMSource source) {
        return null;
    }

    /**
     * Will create an specialized view when there are no repositories or branches found, which contain a Jenkinsfile
     * or other MARKER file.
     */
    @Override
    public View getPrimaryView() {
        if (getItems().isEmpty()) {
            return getWelcomeView();
        }
        return super.getPrimaryView();
    }

    /**
     * Creates a place-holder view when there's no active repositories indexed.
     *
     * @return a place-holder view for when there's no active repositories indexed.
     */
    protected View getWelcomeView() {
        return new OrganizationFolderEmptyView(this);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public View getView(String name) {
        if (name.equals("Welcome")) {
            return getWelcomeView();
        } else {
            return super.getView(name);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getDescription() {
        String description = super.getDescription();
        if (StringUtils.isNotBlank(description)) {
            return description;
        }
        ObjectMetadataAction action = getAction(ObjectMetadataAction.class);
        if (action != null) {
            return action.getObjectDescription();
        }
        return super.getDescription();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getDisplayName() {
        String displayName = getDisplayNameOrNull();
        if (displayName == null) {
            ObjectMetadataAction action = getAction(ObjectMetadataAction.class);
            if (action != null && StringUtils.isNotBlank(action.getObjectDisplayName())) {
                return action.getObjectDisplayName();
            }
        }
        return super.getDisplayName();
    }

    /**
     * Our descriptor
     */
    @Extension
    public static class DescriptorImpl extends AbstractFolderDescriptor {

        /**
         * {@inheritDoc}
         */
        @Override
        public String getDisplayName() {
            if (Jenkins.getActiveInstance().getInitLevel().compareTo(InitMilestone.EXTENSIONS_AUGMENTED) > 0) {
                List<SCMNavigatorDescriptor> navs = remove(ExtensionList.lookup(SCMNavigatorDescriptor.class),
                        SingleSCMNavigator.DescriptorImpl.class);
                if (navs.size() == 1) {
                    return Messages.OrganizationFolder_DisplayName(StringUtils.defaultIfBlank(
                            navs.get(0).getPronoun(), Messages.OrganizationFolder_DefaultPronoun()));
                }
            }
            return Messages.OrganizationFolder_DisplayName(Messages._OrganizationFolder_DefaultPronoun());
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public TopLevelItem newInstance(ItemGroup parent, String name) {
            return new OrganizationFolder(parent, name);
        }

        /**
         * Used to categorize {@link OrganizationFolder} instances.
         *
         * @return A string with the category identifier. {@code TopLevelItemDescriptor#getCategoryId()}
         */
        //@Override TODO once baseline is 2.x
        @NonNull
        public String getCategoryId() {
            return "nested-projects";
        }

        /**
         * A description of this {@link OrganizationFolder}.
         *
         * @return A string with the description. {@code TopLevelItemDescriptor#getDescription()}.
         */
        //@Override TODO once baseline is 2.x
        @NonNull
        public String getDescription() {
            if (Jenkins.getActiveInstance().getInitLevel().compareTo(InitMilestone.EXTENSIONS_AUGMENTED) > 0) {
                List<SCMNavigatorDescriptor> navs = remove(ExtensionList.lookup(SCMNavigatorDescriptor.class),
                        SingleSCMNavigator.DescriptorImpl.class);
                SCMSourceCategory uncategorized = genericScmSourceCategory(navs);

                Locale locale = LocaleProvider.getLocale();
                return Messages.OrganizationFolder_Description(
                        orJoinDisplayName(ExtensionList.lookup(MultiBranchProjectDescriptor.class)),
                        uncategorized.getDisplayName().toString(locale).toLowerCase(locale),
                        orJoinDisplayName(navs));
            }
            Locale locale = LocaleProvider.getLocale();
            return Messages.OrganizationFolder_Description(Messages.OrganizationFolder_DefaultProject(),
                    UncategorizedSCMSourceCategory.DEFAULT.getDisplayName().toString(locale).toLowerCase(locale),
                    Messages.OrganizationFolder_DefaultPronoun());
        }

        //@Override TODO once baseline is 2.x
        public String getIconFilePathPattern() {
            List<SCMNavigatorDescriptor> descriptors = remove(ExtensionList.lookup(SCMNavigatorDescriptor.class),
                    SingleSCMNavigator.DescriptorImpl.class);
            if (descriptors.size() == 1) {
                return descriptors.get(0).getIconFilePathPattern();
            } else {
                return "plugin/branch-api/images/:size/organization-folder.png";
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public String getIconClassName() {
            List<SCMNavigatorDescriptor> descriptors = remove(ExtensionList.lookup(SCMNavigatorDescriptor.class),
                    SingleSCMNavigator.DescriptorImpl.class);
            if (descriptors.size() == 1) {
                return descriptors.get(0).getIconClassName();
            } else {
                return "icon-branch-api-organization-folder";
            }
        }

        /**
         * Joins the {@link Descriptor#getDisplayName()} in a chain of "or".
         * @param navs the {@link Descriptor}s.
         * @return a string representing a disjoint list of the display names.
         */
        private static String orJoinDisplayName(List<? extends Descriptor> navs) {
            String providers;
            switch (navs.size()) {
            case 1:
                providers = navs.get(0).getDisplayName();
                break;
            case 2:
                providers = Messages.OrganizationFolder_OrJoin2(navs.get(0).getDisplayName(),
                        navs.get(1).getDisplayName());
                break;
            case 3:
                providers = Messages.OrganizationFolder_OrJoinN_Last(Messages.OrganizationFolder_OrJoinN_First(
                        navs.get(0).getDisplayName(), navs.get(1).getDisplayName()), navs.get(2).getDisplayName());
                break;
            default:
                String wip = Messages.OrganizationFolder_OrJoinN_First(navs.get(0).getDisplayName(),
                        navs.get(1).getDisplayName());
                for (int i = 2; i < navs.size() - 2; i++) {
                    wip = Messages.OrganizationFolder_OrJoinN_Middle(wip, navs.get(i).getDisplayName());
                }
                providers = Messages.OrganizationFolder_OrJoinN_Last(wip,
                        navs.get(navs.size() - 1).getDisplayName());
                break;
            }
            return providers;
        }

        /**
         * Creates a filtered sublist.
         *
         * @param <T> the type to remove from the base list
         * @param base the base list
         * @param type the type to remove from the base list
         * @return the list will all instances of the supplied type removed.
         */
        @Nonnull
        public static <T> List<T> remove(@Nonnull Iterable<T> base, @Nonnull Class<? extends T> type) {
            List<T> r = new ArrayList<T>();
            for (T i : base) {
                if (!type.isInstance(i))
                    r.add(i);
            }
            return r;
        }

        /**
         * Gets the {@link SCMSourceCategory#isUncategorized()} of a list of {@link SCMNavigatorDescriptor} instances.
         * @param descriptors the {@link SCMNavigatorDescriptor} instances.
         * @return the {@link SCMSourceCategory}.
         */
        private SCMSourceCategory genericScmSourceCategory(List<? extends SCMNavigatorDescriptor> descriptors) {
            List<SCMSourceCategory> sourceCategories = new ArrayList<>();
            for (SCMNavigatorDescriptor d : descriptors) {
                sourceCategories.addAll(d.getCategories());
            }
            SCMSourceCategory uncategorized = UncategorizedSCMSourceCategory.DEFAULT;
            for (SCMSourceCategory c : SCMSourceCategory
                    .simplify(SCMSourceCategory.addUncategorizedIfMissing(sourceCategories)).values()) {
                if (c.isUncategorized()) {
                    uncategorized = c;
                    break;
                }
            }
            return uncategorized;
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public List<FolderIconDescriptor> getIconDescriptors() {
            return Collections.<FolderIconDescriptor>singletonList(
                    Jenkins.getActiveInstance().getDescriptorByType(MetadataActionFolderIcon.DescriptorImpl.class));
        }

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

        @Override
        @NonNull
        public final ChildNameGenerator<OrganizationFolder, ? extends TopLevelItem> childNameGenerator() {
            return ChildNameGeneratorImpl.INSTANCE;
        }

        static {
            IconSet.icons.addIcon(new Icon("icon-branch-api-organization-folder icon-sm",
                    "plugin/branch-api/images/16x16/organization-folder.png", Icon.ICON_SMALL_STYLE));
            IconSet.icons.addIcon(new Icon("icon-branch-api-organization-folder icon-md",
                    "plugin/branch-api/images/24x24/organization-folder.png", Icon.ICON_MEDIUM_STYLE));
            IconSet.icons.addIcon(new Icon("icon-branch-api-organization-folder icon-lg",
                    "plugin/branch-api/images/32x32/organization-folder.png", Icon.ICON_LARGE_STYLE));
            IconSet.icons.addIcon(new Icon("icon-branch-api-organization-folder icon-xlg",
                    "plugin/branch-api/images/48x48/organization-folder.png", Icon.ICON_XLARGE_STYLE));
        }
    }

    private static class ChildNameGeneratorImpl
            extends ChildNameGenerator<OrganizationFolder, MultiBranchProject<?, ?>> {

        private static final ChildNameGeneratorImpl INSTANCE = new ChildNameGeneratorImpl();

        @Override
        @CheckForNull
        public String itemNameFromItem(@NonNull OrganizationFolder parent, @NonNull MultiBranchProject<?, ?> item) {
            ProjectNameProperty property = item.getProperties().get(ProjectNameProperty.class);
            if (property != null) {
                return NameEncoder.encode(property.getName());
            }
            String idealName = idealNameFromItem(parent, item);
            if (idealName != null) {
                return NameEncoder.encode(idealName);
            }
            return null;
        }

        @Override
        @CheckForNull
        public String dirNameFromItem(@NonNull OrganizationFolder parent, @NonNull MultiBranchProject<?, ?> item) {
            ProjectNameProperty property = item.getProperties().get(ProjectNameProperty.class);
            if (property != null) {
                return NameMangler.apply(property.getName());
            }
            String idealName = idealNameFromItem(parent, item);
            if (idealName != null) {
                return NameMangler.apply(idealName);
            }
            return null;
        }

        @Override
        @NonNull
        public String itemNameFromLegacy(@NonNull OrganizationFolder parent, @NonNull String legacyDirName) {
            return NameEncoder.decode(legacyDirName);
        }

        @Override
        @NonNull
        public String dirNameFromLegacy(@NonNull OrganizationFolder parent, @NonNull String legacyDirName) {
            return NameMangler.apply(NameEncoder.decode(legacyDirName));
        }

        @Override
        public void recordLegacyName(OrganizationFolder parent, MultiBranchProject<?, ?> item, String legacyDirName)
                throws IOException {
            item.addProperty(new ProjectNameProperty(legacyDirName));
        }
    }

    /**
     * Our scan.
     */
    public static class OrganizationScan extends FolderComputation<MultiBranchProject<?, ?>> {
        public OrganizationScan(OrganizationFolder folder, FolderComputation<MultiBranchProject<?, ?>> previous) {
            super(folder, previous);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public String getDisplayName() {
            return Messages.OrganizationFolder_OrganizationScan_displayName(getParent().getPronoun());
        }

        @Override
        public void run() {
            long start = System.currentTimeMillis();
            try {
                super.run();
            } finally {
                long end = System.currentTimeMillis();
                LOGGER.log(Level.INFO,
                        "{0} #{1,time,yyyyMMdd.HHmmss} organization scan action completed: {2} in {3}",
                        new Object[] { getParent().getFullName(), start, getResult(),
                                Util.getTimeSpanString(end - start) });
            }
        }

    }

    /**
     * Listens for events from the SCM event system.
     *
     * @since 2.0
     */
    @Extension
    public static class SCMEventListenerImpl extends SCMEventListener {

        private final EventOutputStreams globalEvents = createGlobalEvents();

        private EventOutputStreams createGlobalEvents() {
            File logsDir = new File(Jenkins.getActiveInstance().getRootDir(), "logs");
            if (!logsDir.isDirectory() && !logsDir.mkdirs()) {
                LOGGER.log(Level.WARNING, "Could not create logs directory: {0}", logsDir);
            }
            final File eventsFile = new File(logsDir, OrganizationFolder.class.getName() + ".log");
            if (!eventsFile.isFile()) {
                File oldFile = new File(logsDir.getParent(), eventsFile.getName());
                if (oldFile.isFile()) {
                    if (!oldFile.renameTo(eventsFile)) {
                        FileUtils.deleteQuietly(oldFile);
                    }
                }
            }
            return new EventOutputStreams(new EventOutputStreams.OutputFile() {
                @NonNull
                @Override
                public File get() {
                    return eventsFile;
                }
            }, 250, TimeUnit.MILLISECONDS, 1024, true, 32 * 1024, 5);
        }

        /**
         * The {@link TaskListener} for events that we cannot assign to an organization folder.
         * @return The {@link TaskListener} for events that we cannot assign to an organization folder.
         */
        @Restricted(NoExternalUse.class)
        public StreamTaskListener globalEventsListener() {
            return new StreamBuildListener(globalEvents.get(), Charsets.UTF_8);
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void onSCMHeadEvent(SCMHeadEvent<?> event) {
            try (StreamTaskListener global = globalEventsListener()) {
                String globalEventDescription = StringUtils.defaultIfBlank(event.description(),
                        event.getClass().getName());
                global.getLogger().format("[%tc] Received %s %s event from %s with timestamp %tc%n",
                        System.currentTimeMillis(), globalEventDescription, event.getType().name(),
                        event.getOrigin(), event.getTimestamp());
                int matchCount = 0;
                if (CREATED == event.getType() || UPDATED == event.getType()) {
                    try {
                        for (OrganizationFolder p : Jenkins.getActiveInstance()
                                .getAllItems(OrganizationFolder.class)) {
                            if (!p.isBuildable()) {
                                if (LOGGER.isLoggable(Level.FINER)) {
                                    LOGGER.log(Level.FINER,
                                            "{0} {1} {2,date} {2,time}: Ignoring {3} because it is disabled",
                                            new Object[] { globalEventDescription, event.getType().name(),
                                                    event.getTimestamp(), p.getFullName() });
                                }
                                continue;
                            }
                            // we want to catch when a branch is created / updated and consequently becomes eligible
                            // against the criteria. First check if the event matches one of the navigators
                            SCMNavigator navigator = null;
                            for (SCMNavigator n : p.getSCMNavigators()) {
                                if (event.isMatch(n)) {
                                    matchCount++;
                                    global.getLogger().format("Found match against %s%n", p.getFullName());
                                    navigator = n;
                                    break;
                                }
                            }
                            if (navigator == null) {
                                continue;
                            }
                            // ok, now check if any of the sources are a match... if they are then this event is not our
                            // concern
                            for (SCMSource s : p.getSCMSources()) {
                                if (event.isMatch(s)) {
                                    // already have a source that will see this
                                    global.getLogger().format(
                                            "Project %s already has a corresponding sub-project%n",
                                            p.getFullName());
                                    navigator = null;
                                    break;
                                }
                            }
                            if (navigator != null) {
                                global.getLogger().format("Project %s does not have a corresponding sub-project%n",
                                        p.getFullName());
                                String localEventDescription = StringUtils
                                        .defaultIfBlank(event.descriptionFor(navigator), globalEventDescription);
                                try (StreamTaskListener listener = p.getComputation().createEventsListener();
                                        ChildObserver childObserver = p.openEventsChildObserver()) {
                                    long start = System.currentTimeMillis();
                                    listener.getLogger().format(
                                            "[%tc] Received %s %s event from %s with timestamp %tc%n", start,
                                            localEventDescription, event.getType().name(), event.getOrigin(),
                                            event.getTimestamp());
                                    try {
                                        navigator.visitSources(p.new SCMSourceObserverImpl(listener, childObserver,
                                                navigator, event), event);
                                    } catch (IOException e) {
                                        MultiBranchProject.printStackTrace(e, listener.error(e.getMessage()));
                                    } catch (InterruptedException e) {
                                        MultiBranchProject.printStackTrace(e, listener.error(e.getMessage()));
                                        throw e;
                                    } finally {
                                        long end = System.currentTimeMillis();
                                        listener.getLogger().format(
                                                "[%tc] %s %s event from %s with timestamp %tc processed in %s%n",
                                                end, localEventDescription, event.getType().name(),
                                                event.getOrigin(), event.getTimestamp(),
                                                Util.getTimeSpanString(end - start));
                                    }
                                } catch (IOException e) {
                                    MultiBranchProject.printStackTrace(e, global.error(
                                            "[%tc] %s encountered an error while processing %s %s event from %s with "
                                                    + "timestamp %tc",
                                            System.currentTimeMillis(), ModelHyperlinkNote.encodeTo(p),
                                            globalEventDescription, event.getType().name(), event.getOrigin(),
                                            event.getTimestamp()));
                                } catch (InterruptedException e) {
                                    MultiBranchProject.printStackTrace(e, global.error(
                                            "[%tc] %s was interrupted while processing %s %s event from %s with "
                                                    + "timestamp %tc",
                                            System.currentTimeMillis(), ModelHyperlinkNote.encodeTo(p),
                                            globalEventDescription, event.getType().name(), event.getOrigin(),
                                            event.getTimestamp()));
                                    throw e;
                                }
                            }
                        }
                    } catch (InterruptedException e) {
                        MultiBranchProject.printStackTrace(e,
                                global.error(
                                        "[%tc] Interrupted while processing %s %s event from %s with timestamp %tc",
                                        System.currentTimeMillis(), globalEventDescription, event.getType().name(),
                                        event.getOrigin(), event.getTimestamp()));
                    }
                }
                global.getLogger().format(
                        "[%tc] Finished processing %s %s event from %s with timestamp %tc. Matched %d.%n",
                        System.currentTimeMillis(), globalEventDescription, event.getType().name(),
                        event.getOrigin(), event.getTimestamp(), matchCount);
            } catch (IOException e) {
                LOGGER.log(Level.WARNING, "Could not close global event log file", e);
            }

        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void onSCMNavigatorEvent(SCMNavigatorEvent<?> event) {
            try (StreamTaskListener global = globalEventsListener()) {
                global.getLogger().format("[%tc] Received %s %s event from %s with timestamp %tc%n",
                        System.currentTimeMillis(), event.getClass().getName(), event.getType().name(),
                        event.getOrigin(), event.getTimestamp());
                int matchCount = 0;
                if (UPDATED == event.getType()) {
                    Set<SCMNavigator> matches = new HashSet<>();
                    try {
                        for (OrganizationFolder p : Jenkins.getActiveInstance()
                                .getAllItems(OrganizationFolder.class)) {
                            matches.clear();
                            for (SCMNavigator n : p.getSCMNavigators()) {
                                if (event.isMatch(n)) {
                                    matches.add(n);
                                }
                            }
                            if (!matches.isEmpty()) {
                                matchCount++;
                                try (StreamTaskListener listener = p.getComputation().createEventsListener()) {
                                    Map<SCMNavigator, List<Action>> navigatorActions = new HashMap<>();
                                    for (SCMNavigator navigator : matches) {
                                        try {
                                            List<Action> newActions = navigator.fetchActions(p, event, listener);
                                            List<Action> oldActions = p.state.getActions(navigator);
                                            if (oldActions == null || !oldActions.equals(newActions)) {
                                                navigatorActions.put(navigator, newActions);
                                            }
                                        } catch (IOException e) {
                                            MultiBranchProject.printStackTrace(e,
                                                    listener.error("Could not fetch metadata from %s", navigator));
                                        } catch (InterruptedException e) {
                                            MultiBranchProject.printStackTrace(e, listener.error(e.getMessage()));
                                            throw e;
                                        }
                                    }
                                    // update any persistent actions for the SCMNavigator
                                    if (!navigatorActions.isEmpty()) {
                                        boolean saveProject = false;
                                        for (List<Action> actions : navigatorActions.values()) {
                                            for (Action a : actions) {
                                                // undo any hacks that attached the contributed actions without attribution

                                                saveProject = p.removeActions(a.getClass()) || saveProject;
                                            }
                                        }
                                        BulkChange bc = new BulkChange(p.state);
                                        try {
                                            for (Map.Entry<SCMNavigator, List<Action>> entry : navigatorActions
                                                    .entrySet()) {
                                                p.state.setActions(entry.getKey(), entry.getValue());
                                            }
                                            bc.commit();
                                            if (saveProject) {
                                                p.save();
                                            }
                                        } catch (IOException e) {
                                            MultiBranchProject.printStackTrace(e,
                                                    listener.error("Could not persist updated metadata"));
                                        } finally {
                                            bc.abort();
                                        }
                                    }
                                } catch (IOException e) {
                                    MultiBranchProject.printStackTrace(e, global.error(
                                            "[%tc] %s encountered an error while processing %s %s event from %s with "
                                                    + "timestamp %tc",

                                            System.currentTimeMillis(), ModelHyperlinkNote.encodeTo(p),
                                            event.getClass().getName(), event.getType().name(), event.getOrigin(),
                                            event.getTimestamp()));
                                } catch (InterruptedException e) {
                                    MultiBranchProject.printStackTrace(e, global.error(
                                            "[%tc] %s was interrupted while processing %s %s event from %s with "
                                                    + "timestamp %tc",
                                            System.currentTimeMillis(), ModelHyperlinkNote.encodeTo(p),
                                            event.getClass().getName(), event.getType().name(), event.getOrigin(),
                                            event.getTimestamp()));
                                    throw e;
                                }
                            }
                        }
                    } catch (InterruptedException e) {
                        MultiBranchProject.printStackTrace(e,
                                global.error(
                                        "[%tc] Interrupted while processing %s %s event from %s with timestamp %tc",
                                        System.currentTimeMillis(), event.getClass().getName(),
                                        event.getType().name(), event.getOrigin(), event.getTimestamp()));
                    }
                }
                global.getLogger().format(
                        "[%tc] Finished processing %s %s event from %s with timestamp %tc. Matched %d.%n",
                        System.currentTimeMillis(), event.getClass().getName(), event.getType().name(),
                        event.getOrigin(), event.getTimestamp(), matchCount);
            } catch (IOException e) {
                LOGGER.log(Level.WARNING, "Could not close global event log file", e);
            }
        }

        /**
         * {@inheritDoc}
         */
        @Override
        public void onSCMSourceEvent(SCMSourceEvent<?> event) {
            try (StreamTaskListener global = globalEventsListener()) {
                global.getLogger().format("[%tc] Received %s %s event from %s with timestamp %tc%n",
                        System.currentTimeMillis(), event.getClass().getName(), event.getType().name(),
                        event.getOrigin(), event.getTimestamp());
                int matchCount = 0;
                if (CREATED == event.getType()) {
                    try {
                        for (OrganizationFolder p : Jenkins.getActiveInstance()
                                .getAllItems(OrganizationFolder.class)) {
                            boolean haveMatch = false;
                            for (SCMNavigator n : p.getSCMNavigators()) {
                                if (event.isMatch(n)) {
                                    global.getLogger().format("Found match against %s%n", p.getFullName());
                                    haveMatch = true;
                                    break;
                                }
                            }
                            if (haveMatch) {
                                matchCount++;
                                try (StreamTaskListener listener = p.getComputation().createEventsListener();
                                        ChildObserver childObserver = p.openEventsChildObserver()) {
                                    long start = System.currentTimeMillis();
                                    listener.getLogger().format(
                                            "[%tc] Received %s %s event from %s with timestamp %tc%n", start,
                                            event.getClass().getName(), event.getType().name(), event.getOrigin(),
                                            event.getTimestamp());
                                    try {
                                        for (SCMNavigator n : p.getSCMNavigators()) {
                                            if (event.isMatch(n)) {
                                                try {
                                                    n.visitSources(p.new SCMSourceObserverImpl(listener,
                                                            childObserver, n, event), event);
                                                } catch (IOException e) {
                                                    MultiBranchProject.printStackTrace(e,
                                                            listener.error(e.getMessage()));
                                                }
                                            }
                                        }
                                    } catch (InterruptedException e) {
                                        MultiBranchProject.printStackTrace(e, listener.error(e.getMessage()));
                                        throw e;
                                    } finally {
                                        long end = System.currentTimeMillis();
                                        listener.getLogger().format(
                                                "[%tc] %s %s event from %s with timestamp %tc processed in %s%n",
                                                end, event.getClass().getName(), event.getType().name(),
                                                event.getOrigin(), event.getTimestamp(),
                                                Util.getTimeSpanString(end - start));
                                    }
                                } catch (IOException e) {
                                    MultiBranchProject.printStackTrace(e, global.error(
                                            "[%tc] %s encountered an error while processing %s %s event from %s with "
                                                    + "timestamp %tc",

                                            System.currentTimeMillis(), ModelHyperlinkNote.encodeTo(p),
                                            event.getClass().getName(), event.getType().name(), event.getOrigin(),
                                            event.getTimestamp()));
                                } catch (InterruptedException e) {
                                    MultiBranchProject.printStackTrace(e, global.error(
                                            "[%tc] %s was interrupted while processing %s %s event from %s with "
                                                    + "timestamp %tc",
                                            System.currentTimeMillis(), ModelHyperlinkNote.encodeTo(p),
                                            event.getClass().getName(), event.getType().name(), event.getOrigin(),
                                            event.getTimestamp()));
                                    throw e;
                                }
                            }
                        }
                    } catch (InterruptedException e) {
                        MultiBranchProject.printStackTrace(e,
                                global.error(
                                        "[%tc] Interrupted while processing %s %s event from %s with timestamp %tc",
                                        System.currentTimeMillis(), event.getClass().getName(),
                                        event.getType().name(), event.getOrigin(), event.getTimestamp()));
                    }
                }
                global.getLogger().format(
                        "[%tc] Finished processing %s %s event from %s with timestamp %tc. Matched %d.%n",
                        System.currentTimeMillis(), event.getClass().getName(), event.getType().name(),
                        event.getOrigin(), event.getTimestamp(), matchCount);
            } catch (IOException e) {
                LOGGER.log(Level.WARNING, "Could not close global event log file", e);
            }

        }
    }

    private class SCMSourceObserverImpl extends SCMSourceObserver {
        private final TaskListener listener;
        private final ChildObserver<MultiBranchProject<?, ?>> observer;
        private final SCMEvent<?> event;
        private final SCMNavigator navigator;

        public SCMSourceObserverImpl(TaskListener listener, ChildObserver<MultiBranchProject<?, ?>> observer,
                SCMNavigator navigator, SCMEvent<?> event) {
            this.listener = listener;
            this.observer = observer;
            this.navigator = navigator;
            this.event = event;
        }

        @NonNull
        @Override
        public SCMSourceOwner getContext() {
            return OrganizationFolder.this;
        }

        @NonNull
        @Override
        public TaskListener getListener() {
            return listener;
        }

        @NonNull
        @Override
        public ProjectObserver observe(@NonNull final String projectName) {
            return new ProjectObserver() {
                List<SCMSource> sources = new ArrayList<SCMSource>();

                @Override
                public void addSource(@NonNull SCMSource source) {
                    sources.add(source);
                    source.setOwner(OrganizationFolder.this);
                }

                private List<BranchSource> createBranchSources() {
                    if (sources == null) {
                        throw new IllegalStateException();
                    }
                    List<BranchSource> branchSources = new ArrayList<BranchSource>();
                    for (SCMSource source : sources) {
                        // TODO do we want/need a more general BranchPropertyStrategyFactory?
                        branchSources.add(new BranchSource(source));
                    }
                    sources = null; // make sure complete gets called just once
                    return branchSources;
                }

                @Override
                public void addAttribute(@NonNull String key, Object value)
                        throws IllegalArgumentException, ClassCastException {
                    throw new IllegalArgumentException();
                }

                private boolean recognizes(Map<String, Object> attributes,
                        MultiBranchProjectFactory candidateFactory) throws IOException, InterruptedException {
                    return candidateFactory.recognizes(OrganizationFolder.this, projectName, sources, attributes,
                            event instanceof SCMHeadEvent ? (SCMHeadEvent<?>) event : null, listener);
                }

                @Override
                public void complete() throws IllegalStateException, IOException, InterruptedException {
                    try {
                        MultiBranchProjectFactory factory = null;
                        Map<String, Object> attributes = Collections.<String, Object>emptyMap();
                        for (MultiBranchProjectFactory candidateFactory : projectFactories) {
                            if (recognizes(attributes, candidateFactory)) {
                                factory = candidateFactory;
                                break;
                            }
                        }
                        if (factory == null) {
                            return;
                        }
                        String folderName = NameEncoder.encode(projectName);
                        MultiBranchProject<?, ?> existing = observer.shouldUpdate(folderName);
                        try {
                            if (existing != null) {
                                BulkChange bc = new BulkChange(existing);
                                try {
                                    existing.setSourcesList(createBranchSources());
                                    existing.setOrphanedItemStrategy(getOrphanedItemStrategy());
                                    factory.updateExistingProject(existing, attributes, listener);
                                    ProjectNameProperty property = existing.getProperties()
                                            .get(ProjectNameProperty.class);
                                    if (property == null || !projectName.equals(property.getName())) {
                                        existing.getProperties().remove(ProjectNameProperty.class);
                                        existing.addProperty(new ProjectNameProperty(projectName));
                                    }
                                } finally {
                                    bc.commit();
                                }
                                if (isBuildable()) {
                                    existing.scheduleBuild(cause());
                                }
                                return;
                            }
                            if (!observer.mayCreate(folderName)) {
                                listener.getLogger().println(
                                        "Ignoring duplicate child " + projectName + " named " + folderName);
                                return;
                            }
                            MultiBranchProject<?, ?> project;
                            try (ChildNameGenerator.Trace trace = ChildNameGenerator
                                    .beforeCreateItem(OrganizationFolder.this, folderName, projectName)) {
                                if (getItem(folderName) != null) {
                                    throw new IllegalStateException(
                                            "JENKINS-42511: attempted to redundantly create " + folderName + " in "
                                                    + OrganizationFolder.this);
                                }
                                project = factory.createNewProject(OrganizationFolder.this, folderName, sources,
                                        attributes, listener);
                            }
                            BulkChange bc = new BulkChange(project);
                            try {
                                if (!projectName.equals(folderName)) {
                                    project.setDisplayName(projectName);
                                }
                                project.addProperty(new ProjectNameProperty(projectName));
                                project.setOrphanedItemStrategy(getOrphanedItemStrategy());
                                project.getSourcesList().addAll(createBranchSources());
                                try {
                                    project.addTrigger(new PeriodicFolderTrigger("1d"));
                                } catch (ANTLRException x) {
                                    throw new IllegalStateException(x);
                                }
                            } finally {
                                bc.commit();
                            }
                            observer.created(project);
                            if (isBuildable()) {
                                project.scheduleBuild(cause());
                            }
                        } finally {
                            observer.completed(folderName);
                        }
                    } catch (InterruptedException | IOException x) {
                        throw x;
                    } catch (Exception x) {
                        x.printStackTrace(listener.error("Failed to create or update a subproject " + projectName));
                    }
                }
            };
        }

        @Override
        public void addAttribute(@NonNull String key, Object value)
                throws IllegalArgumentException, ClassCastException {
            throw new IllegalArgumentException();
        }

        private Cause cause() {
            if (event instanceof SCMHeadEvent) {
                return new BranchEventCause(event, ((SCMHeadEvent) event).descriptionFor(navigator));
            }
            if (event instanceof SCMSourceEvent) {
                return new BranchEventCause(event, ((SCMSourceEvent) event).descriptionFor(navigator));
            }
            if (event instanceof SCMNavigatorEvent) {
                return new BranchEventCause(event, ((SCMNavigatorEvent) event).descriptionFor(navigator));
            }
            if (event != null) {
                return new BranchEventCause(event, event.description());
            }
            return new BranchIndexingCause();
        }
    }

    /**
     * Adds the {@link OrganizationFolder.State#getActions()} to {@link OrganizationFolder#getAllActions()}.
     *
     * @since 2.0
     */
    @Extension
    public static class StateActionFactory extends TransientActionFactory<OrganizationFolder> {

        @Override
        public Class<OrganizationFolder> type() {
            return OrganizationFolder.class;
        }

        @Nonnull
        @Override
        public Collection<? extends Action> createFor(@Nonnull OrganizationFolder target) {
            List<Action> result = new ArrayList<>();
            for (List<Action> actions : target.state.getActions().values()) {
                result.addAll(actions);
            }
            return result;
        }
    }

    /**
     * The persisted state.
     *
     * @since 2.0
     */
    private static class State implements Saveable {
        private final transient OrganizationFolder owner;
        /**
         * The {@link SCMNavigator#fetchActions(SCMNavigatorOwner, SCMNavigatorEvent, TaskListener)} for each {@link SCMNavigator} keyed by the digest of the {@link SCMNavigator}.
         */
        private final Map<String, List<Action>> actions = new HashMap<>();

        private State(OrganizationFolder owner) {
            this.owner = owner;
        }

        public synchronized void reset() {
            actions.clear();
        }

        public final XmlFile getStateFile() {
            return new XmlFile(Items.XSTREAM, new File(owner.getRootDir(), "state.xml"));
        }

        public synchronized void load() throws IOException {
            if (getStateFile().exists()) {
                getStateFile().unmarshal(this);
            }
        }

        /**
         * Save the settings to a file.
         */
        @Override
        public void save() throws IOException {
            synchronized (this) {
                if (BulkChange.contains(this)) {
                    return;
                }
                getStateFile().write(this);
            }
            SaveableListener.fireOnChange(this, getStateFile());
        }

        public List<Action> getActions(SCMNavigator navigator) {
            if (owner.getSCMNavigators().contains(navigator)) {
                return Collections.unmodifiableList(Util.fixNull(actions.get(navigator.getId())));
            }
            return null;
        }

        public void setActions(SCMNavigator navigator, List<Action> actions) {
            this.actions.put(navigator.getId(), new ArrayList<Action>(actions));
        }

        public Map<SCMNavigator, List<Action>> getActions() {
            List<SCMNavigator> navigators = owner.getSCMNavigators();
            Map<SCMNavigator, List<Action>> result = new HashMap<>(navigators.size());
            for (SCMNavigator navigator : navigators) {
                result.put(navigator, Collections.unmodifiableList(Util.fixNull(actions.get(navigator.getId()))));
            }
            return result;
        }

        public void setActions(Map<SCMNavigator, List<Action>> actions) {
            Set<String> keys = new HashSet<>();
            for (Map.Entry<SCMNavigator, List<Action>> entry : actions.entrySet()) {
                String id = entry.getKey().getId();
                this.actions.put(id, new ArrayList<Action>(Util.fixNull(entry.getValue())));
                keys.add(id);
            }
            this.actions.keySet().retainAll(keys);
        }
    }
}