org.cloudbees.literate.jenkins.LiterateBranchProject.java Source code

Java tutorial

Introduction

Here is the source code for org.cloudbees.literate.jenkins.LiterateBranchProject.java

Source

/*
 * The MIT License
 *
 * Copyright (c) 2013, 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 org.cloudbees.literate.jenkins;

import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.BulkChange;
import hudson.CopyOnWrite;
import hudson.DescriptorExtensionList;
import hudson.Extension;
import hudson.FilePath;
import hudson.Launcher;
import hudson.Util;
import hudson.XmlFile;
import hudson.init.InitMilestone;
import hudson.init.Initializer;
import hudson.model.AbstractBuild;
import hudson.model.AbstractProject;
import hudson.model.BuildListener;
import hudson.model.Describable;
import hudson.model.Descriptor;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.Items;
import hudson.model.JobProperty;
import hudson.model.Label;
import hudson.model.Project;
import hudson.model.Queue;
import hudson.model.SCMedItem;
import hudson.model.TaskListener;
import hudson.model.TopLevelItem;
import hudson.model.TopLevelItemDescriptor;
import hudson.scm.SCM;
import hudson.security.Permission;
import hudson.tasks.BuildWrapper;
import hudson.tasks.Publisher;
import hudson.util.AlternativeUiTextProvider;
import hudson.util.CopyOnWriteMap;
import hudson.util.DescribableList;
import jenkins.branch.Branch;
import jenkins.branch.BranchProperty;
import jenkins.branch.DescriptorOrder;
import jenkins.branch.ProjectDecorator;
import jenkins.model.Jenkins;
import jenkins.scm.SCMCheckoutStrategy;
import jenkins.scm.SCMCheckoutStrategyDescriptor;
import jenkins.scm.api.SCMHead;
import jenkins.scm.api.SCMRevision;
import jenkins.scm.api.SCMRevisionAction;
import jenkins.scm.api.SCMSource;
import org.apache.commons.lang.StringUtils;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.TokenList;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;

/**
 * Represents a specific branch of a literate build project.
 *
 * @author Stephen Connolly
 */
public class LiterateBranchProject extends Project<LiterateBranchProject, LiterateBranchBuild>
        implements TopLevelItem, SCMedItem, ItemGroup<LiterateEnvironmentProject>, Queue.FlyweightTask {

    /**
     * Hack to prevent the Configure link showing up in the sidebar.
     */
    public static final Permission CONFIGURE = null;

    /**
     * The branch that we are tracking.
     */
    @NonNull
    private Branch branch;

    /**
     * The environments that have been built for this branch.
     */
    @NonNull
    private transient Map<BuildEnvironment, LiterateEnvironmentProject> environments = new CopyOnWriteMap.Tree<BuildEnvironment, LiterateEnvironmentProject>();

    /**
     * The subset of environments that were built for the most recent build.
     */
    @CopyOnWrite
    @NonNull
    private transient volatile Set<BuildEnvironment> activeEnvironments = new TreeSet<BuildEnvironment>();

    /**
     * Constructor.
     *
     * @param parent the parent.
     * @param branch the branch.
     */
    public LiterateBranchProject(@NonNull LiterateMultibranchProject parent, @NonNull Branch branch) {
        super(parent, branch.getName());
        this.branch = branch;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Label getAssignedLabel() {
        return null; // we are a flyweight task, don't care where our "placeholder" builds.
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public String getAssignedLabelString() {
        return null; // we are a flyweight task, don't care where our "placeholder" builds.
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void onLoad(@NonNull ItemGroup<? extends Item> parent, @NonNull String name) throws IOException {
        super.onLoad(parent, name);
        environments = new CopyOnWriteMap.Tree<BuildEnvironment, LiterateEnvironmentProject>();
        getBuildersList().setOwner(this);
        getPublishersList().setOwner(this);
        getBuildWrappersList().setOwner(this);

        rebuildEnvironments(null);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @NonNull
    public LiterateMultibranchProject getParent() {
        return (LiterateMultibranchProject) super.getParent();
    }

    /**
     * Returns the branch.
     *
     * @return the branch.
     */
    public synchronized Branch getBranch() {
        return branch;
    }

    /**
     * Sets the branch.
     *
     * @param branch the branch.
     */
    public synchronized void setBranch(@NonNull Branch branch) {
        branch.getClass();
        this.branch = branch;
    }

    private <T extends Describable<T>> void setDescribableListItems(DescribableList<T, Descriptor<T>> list,
            Collection<T> newList) {
        try {
            list.setOwner(NOOP);
            list.replaceBy(newList);
            list.setOwner(this);
        } catch (IOException e) {
            // should never happen
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @NonNull
    public synchronized String getName() {
        return branch.getName();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @NonNull
    public synchronized SCM getScm() {
        // FIXME scm reference is getting updated through SCMCheckoutStrategyImpl#preCheckout, so it should probably be
        // exposed to LiterateEnvironmentProject instances
        return branch.getScm();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    @NonNull
    public SCMCheckoutStrategy getScmCheckoutStrategy() {
        Jenkins j = Jenkins.getInstance();
        if (j == null) {
            throw new IllegalStateException("Jenkins has not started, or is shutting down"); // TODO 1.590+ getActiveInstance
        }
        return j.getDescriptorByType(SCMCheckoutStrategyImpl.DescriptorImpl.class).getInstance();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public synchronized boolean isBuildable() {
        return super.isBuildable() && branch != null && branch.isBuildable();
    }

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

    /**
     * {@inheritDoc}
     */
    @Override
    @NonNull
    protected Class<LiterateBranchBuild> getBuildClass() {
        return LiterateBranchBuild.class;
    }

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

    /**
     * Gets the directory that holds all the environments.
     *
     * @return the directory that holds all the environments.
     */
    @NonNull
    public File getEnvironmentsDir() {
        return new File(getRootDir(), "environments");
    }

    /**
     * Gets a named item.
     *
     * @param name the name.
     * @return the item.
     */
    @CheckForNull
    public LiterateEnvironmentProject getItem(@NonNull String name) {
        try {
            // fast path for nice environments
            BuildEnvironment fastPath = BuildEnvironment.fromString(name);
            LiterateEnvironmentProject configuration = environments.get(fastPath);
            if (configuration != null) {
                return configuration;
            }
            // need to search in depth as name could still contain an environment that contain ","
            // just where the sort is order preserved
        } catch (IllegalArgumentException e) {
            // need to search in depth as name looks like it contains an environment that contain ","
        }
        // search in depth
        for (Map.Entry<BuildEnvironment, LiterateEnvironmentProject> e : environments.entrySet()) {
            if (e.getKey().getName().equals(name)) {
                return e.getValue();
            }
        }
        return null;
    }

    /**
     * Returns the active items.
     *
     * @return the active items.
     */
    @SuppressWarnings("unused") // accessed by Jelly EL
    public Collection<LiterateEnvironmentProject> getActiveItems() {
        Map<BuildEnvironment, LiterateEnvironmentProject> result = new LinkedHashMap<BuildEnvironment, LiterateEnvironmentProject>(
                environments);
        result.keySet().retainAll(activeEnvironments);
        return result.values();
    }

    /**
     * Returns the active {@link BuildEnvironment}s.
     *
     * @return the active {@link BuildEnvironment}s.
     */
    public Collection<BuildEnvironment> getActiveEnvironments() {
        return activeEnvironments;
    }

    /**
     * {@inheritDoc}
     */
    public Collection<LiterateEnvironmentProject> getItems() {
        return environments.values();
    }

    /**
     * {@inheritDoc}
     */
    public String getUrlChildPrefix() {
        return ".";
    }

    /**
     * {@inheritDoc}
     */
    public File getRootDirFor(LiterateEnvironmentProject child) {
        return getRootDir(child.getEnvironment());
    }

    /**
     * Gets the root directory for a specific environment.
     *
     * @param environment the environment.
     * @return the root directory of that environment.
     */
    private File getRootDir(BuildEnvironment environment) {
        File f = getEnvironmentsDir();
        if (environment.isDefault()) {
            f = new File(f, "env-");
        } else {
            for (String env : environment.getComponents()) {
                f = new File(f, "env-" + Util.rawEncode(env));
            }
        }
        return f;
    }

    /**
     * {@inheritDoc}
     */
    public void onRenamed(LiterateEnvironmentProject item, String oldName, String newName) throws IOException {
        throw new UnsupportedOperationException();
    }

    /**
     * {@inheritDoc}
     */
    public void onDeleted(LiterateEnvironmentProject item) throws IOException {
        // no-op
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Object getDynamic(String token, StaplerRequest req, StaplerResponse rsp) {
        try {
            LiterateEnvironmentProject item = getItem(token);
            if (item != null) {
                return item;
            }
        } catch (IllegalArgumentException _) {
            // failed to parse the token as BuildEnvironment. Must be something else
        }
        return super.getDynamic(token, req, rsp);
    }

    /**
     * Returns the specific environment's project.
     *
     * @param components the environment's components.
     * @return the specific environment's project
     */
    public LiterateEnvironmentProject getEnvironment(Set<String> components) {
        return getEnvironment(new BuildEnvironment(components));
    }

    /**
     * Returns the specific environment's project.
     *
     * @param environment the environment.
     * @return the specific environment's project
     */
    public LiterateEnvironmentProject getEnvironment(BuildEnvironment environment) {
        return environments.get(environment);
    }

    /**
     * Load's the environments from the specified directory. This method gets recursively invoked, so the
     * {@code environments} parameter is used to track the state of the recursion.
     *
     * @param dir         the directory to load from.
     * @param result      the map to store the result in.
     * @param environment the environments to add to all child environments of the directory.
     */
    private void loadEnvironments(File dir, CopyOnWriteMap<BuildEnvironment, LiterateEnvironmentProject> result,
            Set<String> environment) {
        File[] environmentDirs = dir.listFiles(new FileFilter() {
            public boolean accept(File child) {
                return child.isDirectory() && child.getName().startsWith("env-");
            }
        });
        if (environmentDirs == null) {
            return;
        }
        for (File v : environmentDirs) {
            Set<String> c = new TreeSet<String>(environment);
            String id = TokenList.decode(v.getName().substring("env-".length()));
            if (!StringUtils.isBlank(id)) {
                c.add(id);
            }
            try {
                XmlFile config = Items.getConfigFile(v);
                if (config.exists()) {
                    BuildEnvironment env = new BuildEnvironment(c);
                    LiterateEnvironmentProject item = null;
                    if (this.environments != null) {
                        item = this.environments.get(env);
                    }
                    if (item == null) {
                        item = (LiterateEnvironmentProject) config.read();
                        item.setEnvironment(env);
                        item.onLoad(this, env.getName());
                    }
                    result.put(env, item);
                }
            } catch (IOException e) {
                // todo LOGGER.log(Level.WARNING, "Failed to load branch environment " + v, e);
            }
            loadEnvironments(v, result, c);
        }
    }

    /**
     * Rebuilds the set of environments for the specified context.
     *
     * @param context the contex.
     * @return the set of {@link BuildEnvironment}s.
     * @throws IOException if something goes wrong.
     */
    Set<BuildEnvironment> rebuildEnvironments(LiterateBranchBuild context) throws IOException {
        CopyOnWriteMap.Tree<BuildEnvironment, LiterateEnvironmentProject> environments = new CopyOnWriteMap.Tree<BuildEnvironment, LiterateEnvironmentProject>();
        loadEnvironments(getEnvironmentsDir(), environments, Collections.<String>emptySet());
        this.environments = environments;
        if (context == null) {
            context = getLastBuild();
        }
        Iterable<BuildEnvironment> activeBuildEnvironments;
        if (context != null) {
            activeBuildEnvironments = context.getBuildEnvironments();
        } else {
            activeBuildEnvironments = Collections.emptyList();
        }

        Set<BuildEnvironment> activeEnvironments = new TreeSet<BuildEnvironment>();
        for (BuildEnvironment buildEnv : activeBuildEnvironments) {
            activeEnvironments.add(buildEnv);
            LiterateEnvironmentProject config = this.environments.get(buildEnv);
            if (config == null) {
                config = decorate(new LiterateEnvironmentProject(this, buildEnv));
                config.onCreatedFromScratch();
                config.save();
                this.environments.put(buildEnv, config);
            } else {
                decorate(config);
                config.save();
            }
        }
        this.activeEnvironments = activeEnvironments;
        return activeEnvironments;
    }

    /**
     * Decorates the environment project.
     *
     * @param project the project.
     * @return the project for nicer method chaining
     */
    @SuppressWarnings("ConstantConditions")
    public LiterateEnvironmentProject decorate(LiterateEnvironmentProject project) {
        Branch branch = getBranch();
        // HACK ALERT
        // ==========
        // We don't want to trigger a save, so we will do some trickery to inject the new values
        // it would be better if Core gave us some hooks to do this
        BulkChange bc = new BulkChange(project);
        try {
            List<BranchProperty> properties = new ArrayList<BranchProperty>(branch.getProperties());
            Collections.sort(properties, DescriptorOrder.reverse(BranchProperty.class));
            for (BranchProperty property : properties) {
                ProjectDecorator<LiterateEnvironmentProject, LiterateEnvironmentBuild> decorator = property
                        .decorator(project);
                if (decorator != null) {
                    // if Project then we can feed the publishers and build wrappers
                    DescribableList<Publisher, Descriptor<Publisher>> publishersList = project.getPublishersList();
                    DescribableList buildWrappersList = Project.class.cast(project).getBuildWrappersList();
                    List<Publisher> publishers = decorator.publishers(publishersList.toList());
                    List<BuildWrapper> buildWrappers = decorator.buildWrappers(buildWrappersList.toList());
                    publishersList.replaceBy(publishers);
                    buildWrappersList.replaceBy(buildWrappers);
                    // we can always feed the job properties... but just not as easily as we'd like

                    List<JobProperty<? super LiterateEnvironmentProject>> jobProperties = decorator
                            .jobProperties(project.getAllProperties());
                    // HACK: need to replace all properties but no nice method... we will iterate our way through
                    // both removal and addition
                    for (JobProperty<? super LiterateEnvironmentProject> p : project.getAllProperties()) {
                        project.removeProperty(p);
                    }
                    for (JobProperty<? super LiterateEnvironmentProject> p : jobProperties) {
                        project.addProperty(p);
                    }

                    // now apply the final layer
                    decorator.project(project);
                }
            }
        } catch (IOException e) {
            // should be safe to ignore as the BulkChange suppresses the save operation.
        } finally {
            bc.abort();
        }
        return project;
    }

    /**
     * Updates the list of active environments.
     *
     * @param environments the new list of active environments.
     * @throws IOException if something goes wrong.
     */
    public void setActiveEnvironments(List<Set<String>> environments) throws IOException {
        Set<BuildEnvironment> activeEnvironments = new TreeSet<BuildEnvironment>();
        for (Set<String> env : environments) {
            BuildEnvironment buildEnv = new BuildEnvironment(env);
            activeEnvironments.add(buildEnv);
            LiterateEnvironmentProject config = this.environments.get(buildEnv);
            if (config == null) {
                // todo LOGGER.fine("Adding configuration: " + env);
                config = decorate(new LiterateEnvironmentProject(this, buildEnv));
                config.onCreatedFromScratch();
                config.save();
                this.environments.put(buildEnv, config);
            } else {
                decorate(config);
                config.save();
            }
        }
        this.activeEnvironments = activeEnvironments;
    }

    /**
     * We all hate monkey patching!
     */
    @Override
    public boolean checkout(AbstractBuild build, Launcher launcher, BuildListener listener, File changelogFile)
            throws IOException, InterruptedException {
        final Branch branch = getBranch();
        SCMSource source = getParent().getSCMSource(branch.getSourceId());
        if (source != null) {
            SCMHead head = branch.getHead();
            SCMRevision revision = source.fetch(head, listener);
            if (revision != null) {
                build.addAction(new SCMRevisionAction(revision));
                if (revision.isDeterministic()) {
                    SCM scm = source.build(head, revision);

                    FilePath workspace = build.getWorkspace();
                    assert workspace != null : "we are in a build so must have a workspace";
                    workspace.mkdirs();

                    boolean r = scm.checkout(build, launcher, workspace, listener, changelogFile);
                    if (r) {
                        // Only calcRevisionsFromBuild if checkout was successful. Note that modern SCM implementations
                        // won't reach this line anyway, as they throw AbortExceptions on checkout failure.
                        calcPollingBaseline(build, launcher, listener);
                    }
                    return r;
                }
            }
        }
        return super.checkout(build, launcher, listener, changelogFile);
    }

    /**
     * We all hate monkey patching!
     */
    private void calcPollingBaseline(AbstractBuild build, Launcher launcher, TaskListener listener)
            throws IOException, InterruptedException {
        try {
            Method superMethod = AbstractProject.class.getDeclaredMethod("calcPollingBaseline", AbstractBuild.class,
                    Launcher.class, TaskListener.class);
            superMethod.setAccessible(true);
            superMethod.invoke(this, build, launcher, listener);
        } catch (NoSuchMethodException e) {
            // TODO remove screaming ugly hack when method is exposed in base Jenkins
        } catch (InvocationTargetException e) {
            // TODO remove screaming ugly hack when method is exposed in base Jenkins
        } catch (IllegalAccessException e) {
            // TODO remove screaming ugly hack when method is exposed in base Jenkins
        }
    }

    /**
     * {@inheritDoc}
     */
    // TODO - Hack - child items of an item group that is a view container must to implement TopLevelItem
    @Override
    public TopLevelItemDescriptor getDescriptor() {
        Jenkins j = Jenkins.getInstance();
        if (j == null) {
            throw new IllegalStateException("Jenkins has not started, or is shutting down"); // TODO 1.590+ getActiveInstance
        }
        return (TopLevelItemDescriptor) j.getDescriptorOrDie(LiterateBranchProject.class);
    }

    /**
     * Our descriptor
     */
    // TODO - Hack - child items of an item group that is a view container must to implement TopLevelItem

    @Extension
    public static class DescriptorImpl extends AbstractProjectDescriptor {

        /**
         * {@inheritDoc}
         */
        @Override
        public String getDisplayName() {
            return null;
        }

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

        /**
         * Method that removes this descriptor from the list of {@link TopLevelItemDescriptor}s because
         * we don't want to appear as one.
         *
         * @throws Exception if the hack fails.
         */
        // TODO - Hack - child items of an item group that is a view container must to implement TopLevelItem
        @Initializer(after = InitMilestone.JOB_LOADED, before = InitMilestone.COMPLETED)
        @SuppressWarnings("unused") // invoked by Jenkins
        public static void postInitialize() throws Exception {
            DescriptorExtensionList<TopLevelItem, TopLevelItemDescriptor> all = Items.all();
            all.remove(all.get(DescriptorImpl.class));
        }
    }

    /**
     * The branch specific checkout strategy.
     */
    public static class SCMCheckoutStrategyImpl extends SCMCheckoutStrategy {
        /**
         * {@inheritDoc}
         */
        @Override
        public void preCheckout(AbstractBuild<?, ?> build, Launcher launcher, BuildListener listener)
                throws IOException, InterruptedException {
            if (build instanceof LiterateBranchBuild) {
                LiterateBranchProject project = ((LiterateBranchBuild) build).getParent();
                Branch branch = project.getBranch();
                SCMSource source = project.getParent().getSCMSource(branch.getSourceId());
                if (source != null) {
                    SCMRevision revision = source.fetch(branch.getHead(), listener);
                    project.setScm(source.build(branch.getHead(), revision));
                }
            }
            super.preCheckout(build, launcher, listener);
        }

        /**
         * Our descriptor.
         */
        @Extension
        public static class DescriptorImpl extends SCMCheckoutStrategyDescriptor {
            /**
             * Our singleton.
             */
            private final SCMCheckoutStrategy instance = new SCMCheckoutStrategyImpl();

            /**
             * Returns the singleton.
             *
             * @return the singleton.
             */
            public SCMCheckoutStrategy getInstance() {
                return instance;
            }

            /**
             * {@inheritDoc}
             */
            @Override
            public boolean isApplicable(AbstractProject project) {
                return project instanceof LiterateBranchProject;
            }

            /**
             * {@inheritDoc}
             */
            @Override
            public String getDisplayName() {
                return "Literate Build Checkout Strategy";
            }
        }
    }

    /**
     * Give us a nice XML alias
     */
    @Initializer(before = InitMilestone.PLUGINS_STARTED)
    @SuppressWarnings("unused") // called by Jenkins
    public static void registerXStream() {
        Items.XSTREAM.alias("literate-branch", LiterateMultibranchProject.class);
    }
}