com.cloudbees.hudson.plugins.folder.AbstractFolder.java Source code

Java tutorial

Introduction

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

Source

/*
 * The MIT License
 *
 * Copyright 2015-2016 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;

import com.cloudbees.hudson.plugins.folder.computed.ComputedFolder;
import com.cloudbees.hudson.plugins.folder.health.FolderHealthMetric;
import com.cloudbees.hudson.plugins.folder.health.FolderHealthMetricDescriptor;
import com.cloudbees.hudson.plugins.folder.icons.StockFolderIcon;
import com.cloudbees.hudson.plugins.folder.views.AbstractFolderViewHolder;
import com.cloudbees.hudson.plugins.folder.views.DefaultFolderViewHolder;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.AbortException;
import hudson.BulkChange;
import hudson.Extension;
import hudson.Util;
import hudson.XmlFile;
import hudson.init.InitMilestone;
import hudson.init.Initializer;
import hudson.model.AbstractItem;
import hudson.model.Action;
import hudson.model.AllView;
import hudson.model.Descriptor;
import hudson.model.HealthReport;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.ItemGroupMixIn;
import hudson.model.Items;
import hudson.model.Job;
import hudson.model.ModifiableViewGroup;
import hudson.model.Run;
import hudson.model.TaskListener;
import hudson.model.TopLevelItem;
import hudson.model.View;
import hudson.model.ViewGroupMixIn;
import hudson.model.listeners.ItemListener;
import hudson.model.listeners.RunListener;
import hudson.search.CollectionSearchIndex;
import hudson.search.SearchIndexBuilder;
import hudson.search.SearchItem;
import hudson.security.ACL;
import hudson.util.AlternativeUiTextProvider;
import hudson.util.CaseInsensitiveComparator;
import hudson.util.CopyOnWriteMap;
import hudson.util.DescribableList;
import hudson.util.FormApply;
import hudson.util.FormValidation;
import hudson.util.Function1;
import hudson.util.HttpResponses;
import hudson.views.DefaultViewsTabBar;
import hudson.views.ViewsTabBar;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URLEncoder;
import java.text.ParseException;
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.Map;
import java.util.Random;
import java.util.Set;
import java.util.Stack;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.servlet.ServletException;
import jenkins.model.Jenkins;
import jenkins.model.ModelObjectWithChildren;
import jenkins.model.ProjectNamingStrategy;
import jenkins.model.TransientActionFactory;
import net.sf.json.JSONObject;
import org.acegisecurity.AccessDeniedException;
import org.acegisecurity.context.SecurityContext;
import org.acegisecurity.context.SecurityContextHolder;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;
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.QueryParameter;
import org.kohsuke.stapler.StaplerFallback;
import org.kohsuke.stapler.StaplerOverridable;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.interceptor.RequirePOST;

import static hudson.Util.fixEmpty;

/**
 * A general-purpose {@link ItemGroup}.
 * Base for {@link Folder} and {@link ComputedFolder}.
 * <p>
 * <b>Extending Folders UI</b><br>
 * As any other {@link Item} type, folder types support extension of UI via {@link Action}s.
 * These actions can be persisted or added via {@link TransientActionFactory}.
 * See <a href="https://wiki.jenkins-ci.org/display/JENKINS/Action+and+its+family+of+subtypes">this page</a> 
 * for more details about actions.
 * In folders actions provide the following features:
 * <ul>
 *  <li>Left sidepanel hyperlink, which opens the page specified by action's {@code index.jelly}.</li>
 *  <li>Optional summary boxes on the main panel, which may be defined by {@code summary.jelly}.</li>
 * </ul>
 * @since 4.11-beta-1
 */
@SuppressWarnings({ "unchecked", "rawtypes" }) // mistakes in various places
public abstract class AbstractFolder<I extends TopLevelItem> extends AbstractItem implements TopLevelItem,
        ItemGroup<I>, ModifiableViewGroup, StaplerFallback, ModelObjectWithChildren, StaplerOverridable {

    /**
     * Our logger.
     */
    private static final Logger LOGGER = Logger.getLogger(AbstractFolder.class.getName());
    private static final Random ENTROPY = new Random();
    private static final int HEALTH_REPORT_CACHE_REFRESH_MIN = Math.max(10, Math.min(1440,
            Integer.getInteger(AbstractFolder.class.getName() + ".healthReportCacheRefreshMin", 60)));

    private static long loadingTick;
    private static final AtomicInteger jobTotal = new AtomicInteger();
    private static final AtomicInteger jobEncountered = new AtomicInteger();
    private static final AtomicBoolean loadJobTotalRan = new AtomicBoolean();
    private static final int TICK_INTERVAL = 15000;

    @Initializer(before = InitMilestone.JOB_LOADED, fatal = false)
    public static void loadJobTotal() {
        if (!loadJobTotalRan.compareAndSet(false, true)) {
            return; // TODO why does Jenkins run the initializer many times?!
        }
        scan(new File(Jenkins.getActiveInstance().getRootDir(), "jobs"), 0);
        // TODO reset count after reload config from disk (otherwise goes up to 200% etc.)
    }

    private static void scan(File d, int depth) {
        File[] projects = d.listFiles();
        if (projects == null) {
            return;
        }
        for (File project : projects) {
            if (!new File(project, "config.xml").isFile()) {
                continue;
            }
            if (depth > 0) {
                jobTotal.incrementAndGet();
            }
            File jobs = new File(project, "jobs"); // cf. getJobsDir
            if (jobs.isDirectory()) {
                scan(jobs, depth + 1);
            }
        }
    }

    /** Child items, keyed by {@link Item#getName}. */
    protected transient Map<String, I> items = new CopyOnWriteMap.Tree<String, I>(
            CaseInsensitiveComparator.INSTANCE);

    private DescribableList<AbstractFolderProperty<?>, AbstractFolderPropertyDescriptor> properties;

    private /*almost final*/ AbstractFolderViewHolder folderViews;

    /**
     * {@link View}s.
     */
    @Deprecated
    private transient /*almost final*/ CopyOnWriteArrayList<View> views;

    /**
     * Currently active Views tab bar.
     */
    @Deprecated
    private transient volatile ViewsTabBar viewsTabBar;

    /**
     * Name of the primary view.
     */
    @Deprecated
    private transient volatile String primaryView;

    private transient /*almost final*/ ViewGroupMixIn viewGroupMixIn;

    private DescribableList<FolderHealthMetric, FolderHealthMetricDescriptor> healthMetrics;

    private FolderIcon icon;

    private transient volatile long nextHealthReportsRefreshMillis;
    private transient volatile List<HealthReport> healthReports;

    /** Subclasses should also call {@link #init}. */
    protected AbstractFolder(ItemGroup parent, String name) {
        super(parent, name);
    }

    protected void init() {
        if (properties == null) {
            properties = new DescribableList<AbstractFolderProperty<?>, AbstractFolderPropertyDescriptor>(this);
        } else {
            properties.setOwner(this);
        }
        for (AbstractFolderProperty p : properties) {
            p.setOwner(this);
        }
        if (icon == null) {
            icon = newDefaultFolderIcon();
        }
        icon.setOwner(this);

        if (folderViews == null) {
            if (views != null && !views.isEmpty()) {
                if (primaryView != null) {
                    // TODO replace reflection with direct access once baseline core has JENKINS-38606 fix merged
                    try {
                        Method migrateLegacyPrimaryAllViewLocalizedName = AllView.class
                                .getMethod("migrateLegacyPrimaryAllViewLocalizedName", List.class, String.class);
                        primaryView = (String) migrateLegacyPrimaryAllViewLocalizedName.invoke(null, views,
                                primaryView);
                    } catch (NoSuchMethodException e) {
                        // ignore, Jenkins core does not have JENKINS-38606 fix merged
                    } catch (IllegalAccessException e) {
                        // ignore, Jenkins core does not have JENKINS-38606 fix merged
                    } catch (InvocationTargetException e) {
                        // ignore, Jenkins core does not have JENKINS-38606 fix merged
                    }
                }
                folderViews = new DefaultFolderViewHolder(views, primaryView,
                        viewsTabBar == null ? newDefaultViewsTabBar() : viewsTabBar);
            } else {
                folderViews = newFolderViewHolder();
            }
            views = null;
            primaryView = null;
            viewsTabBar = null;
        }
        viewGroupMixIn = new ViewGroupMixIn(this) {
            @Override
            protected List<View> views() {
                return folderViews.getViews();
            }

            @Override
            protected String primaryView() {
                String primaryView = folderViews.getPrimaryView();
                return primaryView == null ? folderViews.getViews().get(0).getViewName() : primaryView;
            }

            @Override
            protected void primaryView(String name) {
                folderViews.setPrimaryView(name);
            }

            @Override
            public void addView(View v) throws IOException {
                if (folderViews.isViewsModifiable()) {
                    super.addView(v);
                }
            }

            @Override
            public boolean canDelete(View view) {
                return folderViews.isViewsModifiable() && super.canDelete(view);
            }

            @Override
            public synchronized void deleteView(View view) throws IOException {
                if (folderViews.isViewsModifiable()) {
                    super.deleteView(view);
                }
            }
        };
        if (healthMetrics == null) {
            List<FolderHealthMetric> metrics = new ArrayList<FolderHealthMetric>();
            for (FolderHealthMetricDescriptor d : FolderHealthMetricDescriptor.all()) {
                FolderHealthMetric metric = d.createDefault();
                if (metric != null) {
                    metrics.add(metric);
                }
            }
            healthMetrics = new DescribableList<FolderHealthMetric, FolderHealthMetricDescriptor>(this, metrics);
        }
    }

    protected DefaultViewsTabBar newDefaultViewsTabBar() {
        return new DefaultViewsTabBar();
    }

    protected AbstractFolderViewHolder newFolderViewHolder() {
        CopyOnWriteArrayList views = new CopyOnWriteArrayList<View>();
        try {
            initViews(views);
        } catch (IOException e) {
            LOGGER.log(Level.WARNING, "Failed to set up the initial view", e);
        }
        return new DefaultFolderViewHolder(views, null, newDefaultViewsTabBar());
    }

    protected FolderIcon newDefaultFolderIcon() {
        return new StockFolderIcon();
    }

    protected void initViews(List<View> views) throws IOException {
        AllView v = new AllView("All", this);
        views.add(v);
    }

    /**
     * {@inheritDoc}
     */
    // TODO remove once baseline has JENKINS-39404
    @Override
    @SuppressWarnings("deprecation")
    public void addAction(Action a) {
        super.getActions().add(a);
    }

    /**
     * {@inheritDoc}
     */
    // TODO remove once baseline has JENKINS-39404
    @Override
    @SuppressWarnings("deprecation")
    public void replaceAction(Action a) {
        addOrReplaceAction(a);
    }

    /**
     * Add an action, replacing any existing actions of the (exact) same class.
     * Note: calls to {@link #getAllActions()} that happen before calls to this method may not see the update.
     * Note: this method does not affect transient actions contributed by a {@link TransientActionFactory}
     *
     * @param a an action to add/replace
     * @return {@code true} if this actions changed as a result of the call
     * @since FIXME
     */
    // TODO remove once baseline has JENKINS-39404
    @SuppressWarnings({ "ConstantConditions", "deprecation" })
    @SuppressFBWarnings("RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE")
    public boolean addOrReplaceAction(@Nonnull Action a) {
        if (a == null) {
            throw new IllegalArgumentException("Action must be non-null");
        }
        // CopyOnWriteArrayList does not support Iterator.remove, so need to do it this way:
        List<Action> old = new ArrayList<Action>(1);
        List<Action> current = super.getActions();
        boolean found = false;
        for (Action a2 : current) {
            if (!found && a.equals(a2)) {
                found = true;
            } else if (a2.getClass() == a.getClass()) {
                old.add(a2);
            }
        }
        current.removeAll(old);
        if (!found) {
            addAction(a);
        }
        return !found || !old.isEmpty();
    }

    /**
     * Remove an action.
     * Note: calls to {@link #getAllActions()} that happen before calls to this method may not see the update.
     * Note: this method does not affect transient actions contributed by a {@link TransientActionFactory}
     *
     * @param a an action to remove (if {@code null} then this will be a no-op)
     * @return {@code true} if this actions changed as a result of the call
     * @since FIXME
     */
    // TODO remove once baseline has JENKINS-39404
    @SuppressWarnings("deprecation")
    public boolean removeAction(@Nullable Action a) {
        if (a == null) {
            return false;
        }
        // CopyOnWriteArrayList does not support Iterator.remove, so need to do it this way:
        return super.getActions().removeAll(Collections.singleton(a));
    }

    /**
     * Removes any actions of the specified type.
     * Note: calls to {@link #getAllActions()} that happen before calls to this method may not see the update.
     * Note: this method does not affect transient actions contributed by a {@link TransientActionFactory}
     *
     * @param clazz the type of actions to remove
     * @return {@code true} if this actions changed as a result of the call
     * @since FIXME
     */
    // TODO remove once baseline has JENKINS-39404
    @SuppressWarnings({ "ConstantConditions", "deprecation" })
    @SuppressFBWarnings("RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE")
    public boolean removeActions(@Nonnull Class<? extends Action> clazz) {
        if (clazz == null) {
            throw new IllegalArgumentException("Action type must be non-null");
        }
        // CopyOnWriteArrayList does not support Iterator.remove, so need to do it this way:
        List<Action> old = new ArrayList<Action>();
        List<Action> current = super.getActions();
        for (Action a : current) {
            if (clazz.isInstance(a)) {
                old.add(a);
            }
        }
        return current.removeAll(old);
    }

    /**
     * Replaces any actions of the specified type by the supplied action.
     * Note: calls to {@link #getAllActions()} that happen before calls to this method may not see the update.
     * Note: this method does not affect transient actions contributed by a {@link TransientActionFactory}
     *
     * @param clazz the type of actions to replace (note that the action you are replacing this with need not extend
     *              this class)
     * @param a     the action to replace with
     * @return {@code true} if this actions changed as a result of the call
     * @since FIXME
     */
    // TODO remove once baseline has JENKINS-39404
    @SuppressWarnings({ "ConstantConditions", "deprecation" })
    @SuppressFBWarnings("RCN_REDUNDANT_NULLCHECK_OF_NONNULL_VALUE")
    public boolean replaceActions(@Nonnull Class<? extends Action> clazz, Action a) {
        if (clazz == null) {
            throw new IllegalArgumentException("Action type must be non-null");
        }
        if (a == null) {
            throw new IllegalArgumentException("Action must be non-null");
        }
        // CopyOnWriteArrayList does not support Iterator.remove, so need to do it this way:
        List<Action> old = new ArrayList<Action>();
        List<Action> current = super.getActions();
        boolean found = false;
        for (Action a1 : current) {
            if (!found && a.equals(a1)) {
                found = true;
            } else if (clazz.isInstance(a1) && !a.equals(a1)) {
                old.add(a1);
            }
        }
        current.removeAll(old);
        if (!found) {
            addAction(a);
        }
        return !(old.isEmpty() && found);
    }

    /**
     * Loads all the child {@link Item}s.
     *
     * @param modulesDir Directory that contains sub-directories for each child item.
     */
    // TODO replace with ItemGroupMixIn.loadChildren once baseline core has JENKINS-41222 merged
    public static <K, V extends TopLevelItem> Map<K, V> loadChildren(AbstractFolder<V> parent, File modulesDir,
            Function1<? extends K, ? super V> key) {
        CopyOnWriteMap.Tree<K, V> configurations = new CopyOnWriteMap.Tree<K, V>();
        if (!modulesDir.isDirectory() && !modulesDir.mkdirs()) { // make sure it exists
            LOGGER.log(Level.SEVERE, "Could not create {0} for folder {1}",
                    new Object[] { modulesDir, parent.getFullName() });
            return configurations;
        }

        File[] subdirs = modulesDir.listFiles(new FileFilter() {
            public boolean accept(File child) {
                return child.isDirectory();
            }
        });
        if (subdirs == null) {
            return configurations;
        }
        final ChildNameGenerator<AbstractFolder<V>, V> childNameGenerator = parent.childNameGenerator();
        Map<String, V> byDirName = new HashMap<String, V>();
        if (parent.items != null) {
            if (childNameGenerator == null) {
                for (V item : parent.items.values()) {
                    byDirName.put(item.getName(), item);
                }
            } else {
                for (V item : parent.items.values()) {
                    String itemName = childNameGenerator.dirNameFromItem(parent, item);
                    if (itemName == null) {
                        itemName = childNameGenerator.dirNameFromLegacy(parent, item.getName());
                    }
                    byDirName.put(itemName, item);
                }
            }
        }
        for (File subdir : subdirs) {
            try {
                boolean legacy;
                String childName;
                if (childNameGenerator == null) {
                    // the directory name is the item name
                    childName = subdir.getName();
                    legacy = false;
                } else {
                    File nameFile = new File(subdir, ChildNameGenerator.CHILD_NAME_FILE);
                    if (nameFile.isFile()) {
                        childName = StringUtils.trimToNull(FileUtils.readFileToString(nameFile, "UTF-8"));
                        if (childName == null) {
                            LOGGER.log(Level.WARNING, "{0} was empty, assuming child name is {1}",
                                    new Object[] { nameFile, subdir.getName() });
                            legacy = true;
                            childName = subdir.getName();
                        } else {
                            legacy = false;
                        }
                    } else {
                        // this is a legacy name
                        legacy = true;
                        childName = subdir.getName();
                    }
                }
                // Try to retain the identity of an existing child object if we can.
                V item = byDirName.get(childName);
                boolean itemNeedsSave = false;
                if (item == null) {
                    XmlFile xmlFile = Items.getConfigFile(subdir);
                    if (xmlFile.exists()) {
                        item = (V) xmlFile.read();
                        String name;
                        if (childNameGenerator == null) {
                            name = subdir.getName();
                        } else {
                            String dirName = childNameGenerator.dirNameFromItem(parent, item);
                            if (dirName == null) {
                                dirName = childNameGenerator.dirNameFromLegacy(parent, childName);
                                BulkChange bc = new BulkChange(item); // suppress any attempt to save as parent not set
                                try {
                                    childNameGenerator.recordLegacyName(parent, item, childName);
                                    itemNeedsSave = true;
                                } catch (IOException e) {
                                    LOGGER.log(Level.WARNING, "Ignoring {0} as could not record legacy name",
                                            subdir);
                                    continue;
                                } finally {
                                    bc.abort();
                                }
                            }
                            if (!subdir.getName().equals(dirName)) {
                                File newSubdir = parent.getRootDirFor(dirName);
                                if (newSubdir.exists()) {
                                    LOGGER.log(Level.WARNING,
                                            "Ignoring {0} as folder naming rules collide with {1}",
                                            new Object[] { subdir, newSubdir });
                                    continue;

                                }
                                LOGGER.log(Level.INFO, "Moving {0} to {1} in accordance with folder naming rules",
                                        new Object[] { subdir, newSubdir });
                                if (!subdir.renameTo(newSubdir)) {
                                    LOGGER.log(Level.WARNING, "Failed to move {0} to {1}. Ignoring this item",
                                            new Object[] { subdir, newSubdir });
                                    continue;
                                }
                            }
                            File nameFile = new File(parent.getRootDirFor(dirName),
                                    ChildNameGenerator.CHILD_NAME_FILE);
                            name = childNameGenerator.itemNameFromItem(parent, item);
                            if (name == null) {
                                name = childNameGenerator.itemNameFromLegacy(parent, childName);
                                FileUtils.writeStringToFile(nameFile, name, "UTF-8");
                                BulkChange bc = new BulkChange(item); // suppress any attempt to save as parent not set
                                try {
                                    childNameGenerator.recordLegacyName(parent, item, childName);
                                    itemNeedsSave = true;
                                } catch (IOException e) {
                                    LOGGER.log(Level.WARNING, "Ignoring {0} as could not record legacy name",
                                            subdir);
                                    continue;
                                } finally {
                                    bc.abort();
                                }
                            } else if (!childName.equals(name) || legacy) {
                                FileUtils.writeStringToFile(nameFile, name, "UTF-8");
                            }
                        }
                        item.onLoad(parent, name);
                    } else {
                        LOGGER.log(Level.WARNING, "could not find file " + xmlFile.getFile());
                        continue;
                    }
                } else {
                    String name;
                    if (childNameGenerator == null) {
                        name = subdir.getName();
                    } else {
                        File nameFile = new File(subdir, ChildNameGenerator.CHILD_NAME_FILE);
                        name = childNameGenerator.itemNameFromItem(parent, item);
                        if (name == null) {
                            name = childNameGenerator.itemNameFromLegacy(parent, childName);
                            FileUtils.writeStringToFile(nameFile, name, "UTF-8");
                            BulkChange bc = new BulkChange(item); // suppress any attempt to save as parent not set
                            try {
                                childNameGenerator.recordLegacyName(parent, item, childName);
                                itemNeedsSave = true;
                            } catch (IOException e) {
                                LOGGER.log(Level.WARNING, "Ignoring {0} as could not record legacy name", subdir);
                                continue;
                            } finally {
                                bc.abort();
                            }
                        } else if (!childName.equals(name) || legacy) {
                            FileUtils.writeStringToFile(nameFile, name, "UTF-8");
                        }
                        if (!subdir.getName().equals(name) && item instanceof AbstractItem
                                && ((AbstractItem) item).getDisplayNameOrNull() == null) {
                            BulkChange bc = new BulkChange(item);
                            try {
                                ((AbstractItem) item).setDisplayName(childName);
                            } finally {
                                bc.abort();
                            }
                        }
                    }
                    item.onLoad(parent, name);
                }
                if (itemNeedsSave) {
                    try {
                        item.save();
                    } catch (IOException e) {
                        LOGGER.log(Level.WARNING, "Could not update {0} after applying folder naming rules",
                                item.getFullName());
                    }
                }
                configurations.put(key.call(item), item);
            } catch (Exception e) {
                Logger.getLogger(ItemGroupMixIn.class.getName()).log(Level.WARNING, "could not load " + subdir, e);
            }
        }

        return configurations;
    }

    protected final I itemsPut(String name, I item) {
        ChildNameGenerator<AbstractFolder<I>, I> childNameGenerator = childNameGenerator();
        if (childNameGenerator != null) {
            File nameFile = new File(getRootDirFor(item), ChildNameGenerator.CHILD_NAME_FILE);
            String oldName;
            if (nameFile.isFile()) {
                try {
                    oldName = StringUtils.trimToNull(FileUtils.readFileToString(nameFile, "UTF-8"));
                } catch (IOException e) {
                    oldName = null;
                }
            } else {
                oldName = null;
            }
            if (!name.equals(oldName)) {
                try {
                    FileUtils.writeStringToFile(nameFile, name, "UTF-8");
                } catch (IOException e) {
                    LOGGER.log(Level.WARNING, "Could not create " + nameFile);
                }
            }
        }
        return items.put(name, item);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onLoad(ItemGroup<? extends Item> parent, String name) throws IOException {
        super.onLoad(parent, name);
        init();
        final Thread t = Thread.currentThread();
        String n = t.getName();
        try {
            if (items == null) {
                // When Jenkins is getting reloaded, we want children being loaded to be able to find existing items that they will be overriding.
                // This is necessary for them to correctly keep the running builds, for example.
                // ItemGroupMixIn.loadChildren handles the rest of this logic.
                Item current = parent.getItem(name);
                if (current != null && current.getClass() == getClass()) {
                    this.items = ((AbstractFolder) current).items;
                }
            }

            final ChildNameGenerator<AbstractFolder<I>, I> childNameGenerator = childNameGenerator();
            items = loadChildren(this, getJobsDir(), new Function1<String, I>() {
                @Override
                public String call(I item) {
                    String fullName = item.getFullName();
                    t.setName("Loading job " + fullName);
                    float percentage = 100.0f * jobEncountered.incrementAndGet() / Math.max(1, jobTotal.get());
                    long now = System.currentTimeMillis();
                    if (loadingTick == 0) {
                        loadingTick = now;
                    } else if (now - loadingTick > TICK_INTERVAL) {
                        LOGGER.log(Level.INFO, String.format("Loading job %s (%.1f%%)", fullName, percentage));
                        loadingTick = now;
                    }
                    if (childNameGenerator == null) {
                        return item.getName();
                    } else {
                        String name = childNameGenerator.itemNameFromItem(AbstractFolder.this, item);
                        if (name == null) {
                            return childNameGenerator.itemNameFromLegacy(AbstractFolder.this, item.getName());
                        }
                        return name;
                    }
                }
            });
        } finally {
            t.setName(n);
        }
    }

    private ChildNameGenerator<AbstractFolder<I>, I> childNameGenerator() {
        return getDescriptor().childNameGenerator();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public AbstractFolderDescriptor getDescriptor() {
        return (AbstractFolderDescriptor) Jenkins.getActiveInstance().getDescriptorOrDie(getClass());
    }

    /**
     * May be used to enumerate or remove properties.
     * To add properties, use {@link #addProperty}.
     */
    public DescribableList<AbstractFolderProperty<?>, AbstractFolderPropertyDescriptor> getProperties() {
        return properties;
    }

    @SuppressWarnings("rawtypes") // else setOwner will not compile
    public void addProperty(AbstractFolderProperty p) throws IOException {
        if (!p.getDescriptor().isApplicable(getClass())) {
            throw new IllegalArgumentException(
                    p.getClass().getName() + " cannot be applied to " + getClass().getName());
        }
        p.setOwner(this);
        properties.add(p);
    }

    /** May be overridden, but {@link #loadJobTotal} will be inaccurate in that case. */
    protected File getJobsDir() {
        return new File(getRootDir(), "jobs");
    }

    protected final File getRootDirFor(String name) {
        return new File(getJobsDir(), name);
    }

    @Override
    public File getRootDirFor(I child) {
        ChildNameGenerator<AbstractFolder<I>, I> childNameGenerator = childNameGenerator();
        if (childNameGenerator == null) {
            return getRootDirFor(child.getName());
        }
        String name = childNameGenerator.dirNameFromItem(this, child);
        if (name == null) {
            name = childNameGenerator.dirNameFromLegacy(this, child.getName());
        }
        return getRootDirFor(name);
    }

    /**
     * It is unwise to override this, lest links to children from nondefault {@link View}s break.
     * TODO remove this warning if and when JENKINS-35243 is fixed in the baseline.
     * {@inheritDoc}
     */
    @Override
    public String getUrlChildPrefix() {
        return "job";
    }

    /**
     * For URL binding.
     * @see #getUrlChildPrefix
     */
    public I getJob(String name) {
        return getItem(name);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getPronoun() {
        return AlternativeUiTextProvider.get(PRONOUN, this, getDescriptor().getDisplayName());
    }

    /**
     * Overrides from job properties.
     */
    @Override
    public Collection<?> getOverrides() {
        List<Object> r = new ArrayList<Object>();
        for (AbstractFolderProperty<?> p : properties) {
            r.addAll(p.getItemContainerOverrides());
        }
        return r;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void addView(View v) throws IOException {
        viewGroupMixIn.addView(v);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public boolean canDelete(View view) {
        return viewGroupMixIn.canDelete(view);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void deleteView(View view) throws IOException {
        viewGroupMixIn.deleteView(view);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public View getView(String name) {
        return viewGroupMixIn.getView(name);
    }

    /**
     * {@inheritDoc}
     */
    @Exported
    @Override
    public Collection<View> getViews() {
        return viewGroupMixIn.getViews();
    }

    public AbstractFolderViewHolder getFolderViews() {
        return folderViews;
    }

    public void resetFolderViews() {
        folderViews = newFolderViewHolder();
    }

    /**
     * {@inheritDoc}
     */
    @Exported
    @Override
    public View getPrimaryView() {
        return viewGroupMixIn.getPrimaryView();
    }

    public void setPrimaryView(View v) {
        if (folderViews.isPrimaryModifiable()) {
            folderViews.setPrimaryView(v.getViewName());
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onViewRenamed(View view, String oldName, String newName) {
        viewGroupMixIn.onViewRenamed(view, oldName, newName);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ViewsTabBar getViewsTabBar() {
        return folderViews.getTabBar();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ItemGroup<? extends TopLevelItem> getItemGroup() {
        return this;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<Action> getViewActions() {
        return Collections.emptyList();
    }

    /**
     * Fallback to the primary view.
     */
    @Override
    public View getStaplerFallback() {
        return getPrimaryView();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected SearchIndexBuilder makeSearchIndex() {
        return super.makeSearchIndex().add(new CollectionSearchIndex<TopLevelItem>() {
            /**
             * {@inheritDoc}
             */
            @Override
            protected SearchItem get(String key) {
                return Jenkins.getActiveInstance().getItem(key, grp());
            }

            /**
             * {@inheritDoc}
             */
            @Override
            protected Collection<TopLevelItem> all() {
                return Items.getAllItems(grp(), TopLevelItem.class);
            }

            /**
             * {@inheritDoc}
             */
            @Override
            protected String getName(TopLevelItem j) {
                return j.getRelativeNameFrom(grp());
            }

            /** Disambiguates calls that otherwise would match {@link Item} too. */
            private ItemGroup<?> grp() {
                return AbstractFolder.this;
            }
        });
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ContextMenu doChildrenContextMenu(StaplerRequest request, StaplerResponse response) {
        ContextMenu menu = new ContextMenu();
        for (View view : getViews()) {
            menu.add(view.getAbsoluteUrl(), view.getDisplayName());
        }
        return menu;
    }

    public synchronized void doCreateView(StaplerRequest req, StaplerResponse rsp)
            throws IOException, ServletException, ParseException, Descriptor.FormException {
        checkPermission(View.CREATE);
        addView(View.create(req, rsp, this));
    }

    /**
     * Checks if a top-level view with the given name exists.
     */
    public FormValidation doViewExistsCheck(@QueryParameter String value) {
        checkPermission(View.CREATE);

        String view = fixEmpty(value);
        if (view == null) {
            return FormValidation.ok();
        }

        if (getView(view) == null) {
            return FormValidation.ok();
        } else {
            return FormValidation.error(jenkins.model.Messages.Hudson_ViewAlreadyExists(view));
        }
    }

    /**
     * Get the current health report for a folder.
     *
     * @return the health report. Never returns null
     */
    public HealthReport getBuildHealth() {
        List<HealthReport> reports = getBuildHealthReports();
        return reports.isEmpty() ? new HealthReport() : reports.get(0);
    }

    /**
     * Invalidates the cache of build health reports.
     *
     * @since FIXME
     */
    public void invalidateBuildHealthReports() {
        healthReports = null;
    }

    @Exported(name = "healthReport")
    public List<HealthReport> getBuildHealthReports() {
        if (healthMetrics == null || healthMetrics.isEmpty()) {
            return Collections.<HealthReport>emptyList();
        }
        List<HealthReport> reports = healthReports;
        if (reports != null && nextHealthReportsRefreshMillis > System.currentTimeMillis()) {
            // cache is still valid
            return reports;
        }
        // ensure we refresh on average once every HEALTH_REPORT_CACHE_REFRESH_MIN but not all at once
        nextHealthReportsRefreshMillis = System.currentTimeMillis()
                + TimeUnit.MINUTES.toMillis(HEALTH_REPORT_CACHE_REFRESH_MIN * 3 / 4)
                + ENTROPY.nextInt((int) TimeUnit.MINUTES.toMillis(HEALTH_REPORT_CACHE_REFRESH_MIN / 2));
        reports = new ArrayList<HealthReport>();
        List<FolderHealthMetric.Reporter> reporters = new ArrayList<FolderHealthMetric.Reporter>(
                healthMetrics.size());
        boolean recursive = false;
        boolean topLevelOnly = true;
        for (FolderHealthMetric metric : healthMetrics) {
            recursive = recursive || metric.getType().isRecursive();
            topLevelOnly = topLevelOnly && metric.getType().isTopLevelItems();
            reporters.add(metric.reporter());
        }
        for (AbstractFolderProperty<?> p : getProperties()) {
            for (FolderHealthMetric metric : p.getHealthMetrics()) {
                recursive = recursive || metric.getType().isRecursive();
                topLevelOnly = topLevelOnly && metric.getType().isTopLevelItems();
                reporters.add(metric.reporter());
            }
        }
        if (recursive) {
            Stack<Iterable<? extends Item>> stack = new Stack<Iterable<? extends Item>>();
            stack.push(getItems());
            if (topLevelOnly) {
                while (!stack.isEmpty()) {
                    for (Item item : stack.pop()) {
                        if (item instanceof TopLevelItem) {
                            for (FolderHealthMetric.Reporter reporter : reporters) {
                                reporter.observe(item);
                            }
                            if (item instanceof Folder) {
                                stack.push(((Folder) item).getItems());
                            }
                        }
                    }
                }
            } else {
                while (!stack.isEmpty()) {
                    for (Item item : stack.pop()) {
                        for (FolderHealthMetric.Reporter reporter : reporters) {
                            reporter.observe(item);
                        }
                        if (item instanceof Folder) {
                            stack.push(((Folder) item).getItems());
                        }
                    }
                }
            }
        } else {
            for (Item item : getItems()) {
                for (FolderHealthMetric.Reporter reporter : reporters) {
                    reporter.observe(item);
                }
            }
        }
        for (FolderHealthMetric.Reporter reporter : reporters) {
            reports.addAll(reporter.report());
        }
        for (AbstractFolderProperty<?> p : getProperties()) {
            reports.addAll(p.getHealthReports());
        }

        Collections.sort(reports);
        healthReports = reports; // idempotent write
        return reports;
    }

    public DescribableList<FolderHealthMetric, FolderHealthMetricDescriptor> getHealthMetrics() {
        return healthMetrics;
    }

    public HttpResponse doLastBuild(StaplerRequest req) {
        return HttpResponses.redirectToDot();
    }

    /**
     * Gets the icon used for this folder.
     */
    public FolderIcon getIcon() {
        return icon;
    }

    public void setIcon(FolderIcon icon) {
        this.icon = icon;
        icon.setOwner(this);
    }

    public FolderIcon getIconColor() {
        return icon;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Collection<? extends Job> getAllJobs() {
        Set<Job> jobs = new HashSet<Job>();
        for (Item i : getItems()) {
            jobs.addAll(i.getAllJobs());
        }
        return jobs;
    }

    /**
     * {@inheritDoc}
     */
    @Exported(name = "jobs")
    @Override
    public Collection<I> getItems() {
        List<I> viewableItems = new ArrayList<I>();
        for (I item : items.values()) {
            if (item.hasPermission(Item.READ)) {
                viewableItems.add(item);
            }
        }
        return viewableItems;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public I getItem(String name) throws AccessDeniedException {
        if (items == null) {
            return null;
        }
        I item = items.get(name);
        if (item == null) {
            return null;
        }
        if (!item.hasPermission(Item.READ)) {
            if (item.hasPermission(Item.DISCOVER)) {
                throw new AccessDeniedException("Please log in to access " + name);
            }
            return null;
        }
        return item;
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("deprecation")
    @Override
    public void onRenamed(I item, String oldName, String newName) throws IOException {
        items.remove(oldName);
        items.put(newName, item);
        // For compatibility with old views:
        for (View v : folderViews.getViews()) {
            v.onJobRenamed(item, oldName, newName);
        }
        save();
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings("deprecation")
    @Override
    public void onDeleted(I item) throws IOException {
        ItemListener.fireOnDeleted(item);
        items.remove(item.getName());
        // For compatibility with old views:
        for (View v : folderViews.getViews()) {
            v.onJobRenamed(item, item.getName(), null);
        }
        save();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void delete() throws IOException, InterruptedException {
        // Some parts copied from AbstractItem.
        checkPermission(DELETE);
        // delete individual items first
        // (disregard whether they would be deletable in isolation)
        // JENKINS-34939: do not hold the monitor on this folder while deleting them
        // (thus we cannot do this inside performDelete)
        SecurityContext orig = ACL.impersonate(ACL.SYSTEM);
        try {
            for (Item i : new ArrayList<Item>(items.values())) {
                try {
                    i.delete();
                } catch (AbortException e) {
                    throw (AbortException) new AbortException(
                            "Failed to delete " + i.getFullDisplayName() + " : " + e.getMessage()).initCause(e);
                } catch (IOException e) {
                    throw new IOException("Failed to delete " + i.getFullDisplayName(), e);
                }
            }
        } finally {
            SecurityContextHolder.setContext(orig);
        }
        synchronized (this) {
            performDelete();
        }
        getParent().onDeleted(AbstractFolder.this);
        Jenkins.getActiveInstance().rebuildDependencyGraphAsync();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public synchronized void save() throws IOException {
        if (folderViews != null) {
            folderViews.invalidateCaches();
        }
        if (BulkChange.contains(this)) {
            return;
        }
        super.save();
        // TODO should this not just be done in AbstractItem?
        ItemListener.fireOnUpdated(this);
    }

    /**
     * Renames this item container.
     */
    @Override
    public void renameTo(String newName) throws IOException {
        super.renameTo(newName);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public synchronized void doSubmitDescription(StaplerRequest req, StaplerResponse rsp)
            throws IOException, ServletException {
        getPrimaryView().doSubmitDescription(req, rsp);
    }

    @Restricted(NoExternalUse.class)
    @RequirePOST
    public void doConfigSubmit(StaplerRequest req, StaplerResponse rsp)
            throws IOException, ServletException, Descriptor.FormException {
        checkPermission(CONFIGURE);

        req.setCharacterEncoding("UTF-8");
        JSONObject json = req.getSubmittedForm();

        BulkChange bc = new BulkChange(this);
        try {
            description = json.getString("description");
            displayName = Util.fixEmpty(json.optString("displayNameOrNull"));
            if (folderViews.isTabBarModifiable() && json.has("viewsTabBar")) {
                folderViews.setTabBar(req.bindJSON(ViewsTabBar.class, json.getJSONObject("viewsTabBar")));
            }

            if (folderViews.isPrimaryModifiable() && json.has("primaryView")) {
                folderViews.setPrimaryView(json.getString("primaryView"));
            }

            properties.rebuild(req, json, getDescriptor().getPropertyDescriptors());
            for (AbstractFolderProperty p : properties) {
                p.setOwner(this);
            }

            healthMetrics.replaceBy(req.bindJSONToList(FolderHealthMetric.class, json.get("healthMetrics")));

            icon = req.bindJSON(FolderIcon.class, req.getSubmittedForm().getJSONObject("icon"));
            icon.setOwner(this);

            submit(req, rsp);

            save();
            bc.commit();
        } finally {
            bc.abort();
        }

        // TODO boilerplate
        String newName = json.getString("name");
        ProjectNamingStrategy namingStrategy = Jenkins.getActiveInstance().getProjectNamingStrategy();
        if (newName != null && !newName.equals(name)) {
            // check this error early to avoid HTTP response splitting.
            Jenkins.checkGoodName(newName);
            namingStrategy.checkName(newName);
            rsp.sendRedirect("rename?newName=" + URLEncoder.encode(newName, "UTF-8"));
        } else {
            if (namingStrategy.isForceExistingJobs()) {
                namingStrategy.checkName(name);
            }
            FormApply.success(getSuccessfulDestination()).generateResponse(req, rsp, this);
        }
    }

    /**
     * Where user will be redirected after creating or reconfiguring a {@code AbstractFolder}.
     *
     * @return A string that represents the redirect location URL.
     *
     * @see javax.servlet.http.HttpServletResponse#sendRedirect(String)
     */
    @Restricted(NoExternalUse.class)
    @Nonnull
    protected String getSuccessfulDestination() {
        return ".";
    }

    protected void submit(StaplerRequest req, StaplerResponse rsp)
            throws IOException, ServletException, Descriptor.FormException {
    }

    // TODO boilerplate like this should not be necessary: JENKINS-22936
    @Restricted(DoNotUse.class)
    @RequirePOST
    public void doDoRename(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {

        if (!hasPermission(CONFIGURE)) {
            // rename is essentially delete followed by a create
            checkPermission(CREATE);
            checkPermission(DELETE);
        }

        String newName = req.getParameter("newName");
        Jenkins.checkGoodName(newName);

        String blocker = renameBlocker();
        if (blocker != null) {
            rsp.sendRedirect("rename?newName=" + URLEncoder.encode(newName, "UTF-8") + "&blocker="
                    + URLEncoder.encode(blocker, "UTF-8"));
            return;
        }

        renameTo(newName);
        rsp.sendRedirect2("../" + newName);
    }

    /**
     * Allows a subclass to block renames under dynamic conditions.
     * @return a message if rename should currently be prohibited, or null to allow
     */
    @CheckForNull
    protected String renameBlocker() {
        for (Job<?, ?> job : getAllJobs()) {
            if (job.isBuilding()) {
                return "Unable to rename a folder while a job inside it is building.";
            }
        }
        return null;
    }

    private static void invalidateBuildHealthReports(Item item) {
        while (item != null) {
            if (item instanceof AbstractFolder) {
                ((AbstractFolder) item).invalidateBuildHealthReports();
            }
            if (item.getParent() instanceof Item) {
                item = (Item) item.getParent();
            } else {
                break;
            }
        }
    }

    @Extension
    public static class ItemListenerImpl extends ItemListener {
        @Override
        public void onCreated(Item item) {
            invalidateBuildHealthReports(item);
        }

        @Override
        public void onCopied(Item src, Item item) {
            invalidateBuildHealthReports(item);
        }

        @Override
        public void onDeleted(Item item) {
            invalidateBuildHealthReports(item);
        }

        @Override
        public void onRenamed(Item item, String oldName, String newName) {
            invalidateBuildHealthReports(item);
        }

        @Override
        public void onLocationChanged(Item item, String oldFullName, String newFullName) {
            invalidateBuildHealthReports(item);
        }

        @Override
        public void onUpdated(Item item) {
            invalidateBuildHealthReports(item);
        }

    }

    @Extension
    public static class RunListenerImpl extends RunListener<Run> {
        @Override
        public void onCompleted(Run run, @Nonnull TaskListener listener) {
            invalidateBuildHealthReports(run.getParent());
        }

        @Override
        public void onFinalized(Run run) {
            invalidateBuildHealthReports(run.getParent());
        }

        @Override
        public void onStarted(Run run, TaskListener listener) {
            invalidateBuildHealthReports(run.getParent());
        }

        @Override
        public void onDeleted(Run run) {
            invalidateBuildHealthReports(run.getParent());
        }
    }

}