hudson.PluginManager.java Source code

Java tutorial

Introduction

Here is the source code for hudson.PluginManager.java

Source

/*
 * The MIT License
 * 
 * Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Stephen Connolly, Tom Huybrechts
 * 
 * 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 hudson;

import hudson.model.*;
import hudson.util.Service;

import java.util.Enumeration;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.logging.LogFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.io.FileUtils;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.WebApp;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.HttpRedirect;

/**
 * Manages {@link PluginWrapper}s.
 *
 * @author Kohsuke Kawaguchi
 */
public final class PluginManager extends AbstractModelObject {
    /**
     * All discovered plugins.
     */
    private final List<PluginWrapper> plugins = new ArrayList<PluginWrapper>();

    /**
     * All active plugins.
     */
    private final List<PluginWrapper> activePlugins = new ArrayList<PluginWrapper>();

    private final List<FailedPlugin> failedPlugins = new ArrayList<FailedPlugin>();

    /**
     * Plug-in root directory.
     */
    public final File rootDir;

    public final ServletContext context;

    /**
     * {@link ClassLoader} that can load all the publicly visible classes from plugins
     * (and including the classloader that loads Hudson itself.)
     *
     */
    // implementation is minimal --- just enough to run XStream
    // and load plugin-contributed classes.
    public final ClassLoader uberClassLoader = new UberClassLoader();

    /**
     * Once plugin is uploaded, this flag becomes true.
     * This is used to report a message that Hudson needs to be restarted
     * for new plugins to take effect.
     */
    public volatile boolean pluginUploaded = false;

    /**
     * Strategy for creating and initializing plugins
     */
    private PluginStrategy strategy;

    public PluginManager(ServletContext context) {
        this.context = context;
        // JSON binding needs to be able to see all the classes from all the plugins
        WebApp.get(context).setClassLoader(uberClassLoader);

        rootDir = new File(Hudson.getInstance().getRootDir(), "plugins");
        if (!rootDir.exists())
            rootDir.mkdirs();

        loadBundledPlugins();

        File[] archives = rootDir.listFiles(new FilenameFilter() {
            public boolean accept(File dir, String name) {
                return name.endsWith(".hpi") // plugin jar file
                        || name.endsWith(".hpl"); // linked plugin. for debugging.
            }
        });

        if (archives == null) {
            LOGGER.severe(
                    "Hudson is unable to create " + rootDir + "\nPerhaps its security privilege is insufficient");
            return;
        }

        strategy = createPluginStrategy();

        // load plugins from a system property, for use in the "mvn hudson-dev:run"
        List<File> archivesList = new ArrayList<File>(Arrays.asList(archives));
        String hplProperty = System.getProperty("hudson.bundled.plugins");
        if (hplProperty != null) {
            for (String hplLocation : hplProperty.split(",")) {
                File hpl = new File(hplLocation.trim());
                if (hpl.exists())
                    archivesList.add(hpl);
                else
                    LOGGER.warning("bundled plugin " + hplLocation + " does not exist");
            }
        }

        for (File arc : archivesList) {
            try {
                PluginWrapper p = strategy.createPluginWrapper(arc);
                plugins.add(p);
                if (p.isActive())
                    activePlugins.add(p);
            } catch (IOException e) {
                failedPlugins.add(new FailedPlugin(arc.getName(), e));
                LOGGER.log(Level.SEVERE, "Failed to load a plug-in " + arc, e);
            }
        }

        for (PluginWrapper p : activePlugins.toArray(new PluginWrapper[activePlugins.size()]))
            try {
                strategy.load(p);
            } catch (IOException e) {
                failedPlugins.add(new FailedPlugin(p.getShortName(), e));
                LOGGER.log(Level.SEVERE, "Failed to load a plug-in " + p.getShortName(), e);
                activePlugins.remove(p);
                plugins.remove(p);
            }
    }

    /**
     * Called immediately after the construction.
     * This is a separate method so that code executed from here will see a valid value in
     * {@link Hudson#pluginManager}. 
     */
    public void initialize() {
        for (PluginWrapper p : activePlugins.toArray(new PluginWrapper[activePlugins.size()])) {
            strategy.initializeComponents(p);
            try {
                p.getPlugin().postInitialize();
            } catch (Exception e) {
                failedPlugins.add(new FailedPlugin(p.getShortName(), e));
                LOGGER.log(Level.SEVERE, "Failed to post-initialize a plug-in " + p.getShortName(), e);
                activePlugins.remove(p);
                plugins.remove(p);
            }
        }
    }

    /**
     * If the war file has any "/WEB-INF/plugins/*.hpi", extract them into the plugin directory.
     */
    private void loadBundledPlugins() {
        // this is used in tests, when we want to override the default bundled plugins with .hpl versions
        if (System.getProperty("hudson.bundled.plugins") != null) {
            return;
        }
        Set paths = context.getResourcePaths("/WEB-INF/plugins");
        if (paths == null)
            return; // crap
        for (String path : (Set<String>) paths) {
            String fileName = path.substring(path.lastIndexOf('/') + 1);
            if (fileName.length() == 0) {
                // see http://www.nabble.com/404-Not-Found-error-when-clicking-on-help-td24508544.html
                // I suspect some containers are returning directory names.
                continue;
            }
            try {
                URL url = context.getResource(path);
                long lastModified = url.openConnection().getLastModified();
                File file = new File(rootDir, fileName);
                if (!file.exists() || file.lastModified() != lastModified) {
                    FileUtils.copyURLToFile(url, file);
                    file.setLastModified(url.openConnection().getLastModified());
                    // lastModified is set for two reasons:
                    // - to avoid unpacking as much as possible, but still do it on both upgrade and downgrade
                    // - to make sure the value is not changed after each restart, so we can avoid
                    // unpacking the plugin itself in ClassicPluginStrategy.explode
                }
            } catch (IOException e) {
                LOGGER.log(Level.SEVERE, "Failed to extract the bundled plugin " + fileName, e);
            }
        }
    }

    /**
     * Creates a hudson.PluginStrategy, looking at the corresponding system property. 
     */
    private PluginStrategy createPluginStrategy() {
        String strategyName = System.getProperty(PluginStrategy.class.getName());
        if (strategyName != null) {
            try {
                Class<?> klazz = getClass().getClassLoader().loadClass(strategyName);
                Object strategy = klazz.getConstructor(PluginManager.class).newInstance(this);
                if (strategy instanceof PluginStrategy) {
                    LOGGER.info("Plugin strategy: " + strategyName);
                    return (PluginStrategy) strategy;
                } else {
                    LOGGER.warning(
                            "Plugin strategy (" + strategyName + ") is not an instance of hudson.PluginStrategy");
                }
            } catch (ClassNotFoundException e) {
                LOGGER.warning("Plugin strategy class not found: " + strategyName);
            } catch (Exception e) {
                LOGGER.log(Level.WARNING, "Could not instantiate plugin strategy: " + strategyName
                        + ". Falling back to ClassicPluginStrategy", e);
            }
            LOGGER.info("Falling back to ClassicPluginStrategy");
        }

        // default and fallback
        return new ClassicPluginStrategy(this);
    }

    public PluginStrategy getPluginStrategy() {
        return strategy;
    }

    /**
      * Retrurns true if any new plugin was added, which means a restart is required for the change to take effect.
      */
    public boolean isPluginUploaded() {
        return pluginUploaded;
    }

    public List<PluginWrapper> getPlugins() {
        return plugins;
    }

    public List<FailedPlugin> getFailedPlugins() {
        return failedPlugins;
    }

    public PluginWrapper getPlugin(String shortName) {
        for (PluginWrapper p : plugins) {
            if (p.getShortName().equals(shortName))
                return p;
        }
        return null;
    }

    /**
     * Get the plugin instance that implements a specific class, use to find your plugin singleton.
     * Note: beware the classloader fun.
     * @param pluginClazz The class that your plugin implements.
     * @return The plugin singleton or <code>null</code> if for some reason the plugin is not loaded.
     */
    public PluginWrapper getPlugin(Class<? extends Plugin> pluginClazz) {
        for (PluginWrapper p : plugins) {
            if (pluginClazz.isInstance(p.getPlugin()))
                return p;
        }
        return null;
    }

    /**
     * Get the plugin instances that extend a specific class, use to find similar plugins.
     * Note: beware the classloader fun.
     * @param pluginSuperclass The class that your plugin is derived from.
     * @return The list of plugins implementing the specified class.
     */
    public List<PluginWrapper> getPlugins(Class<? extends Plugin> pluginSuperclass) {
        List<PluginWrapper> result = new ArrayList<PluginWrapper>();
        for (PluginWrapper p : plugins) {
            if (pluginSuperclass.isInstance(p.getPlugin()))
                result.add(p);
        }
        return Collections.unmodifiableList(result);
    }

    public String getDisplayName() {
        return "Plugin Manager";
    }

    public String getSearchUrl() {
        return "pluginManager";
    }

    /**
     * Discover all the service provider implementations of the given class,
     * via <tt>META-INF/services</tt>.
     */
    public <T> Collection<Class<? extends T>> discover(Class<T> spi) {
        Set<Class<? extends T>> result = new HashSet<Class<? extends T>>();

        for (PluginWrapper p : activePlugins) {
            Service.load(spi, p.classLoader, result);
        }

        return result;
    }

    /**
     * Orderly terminates all the plugins.
     */
    public void stop() {
        for (PluginWrapper p : activePlugins)
            p.stop();
        // Work around a bug in commons-logging.
        // See http://www.szegedi.org/articles/memleak.html
        LogFactory.release(uberClassLoader);
    }

    /**
     * Performs the installation of the plugins.
     */
    public void doInstall(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
        Enumeration<String> en = req.getParameterNames();
        while (en.hasMoreElements()) {
            String n = en.nextElement();
            if (n.startsWith("plugin.")) {
                n = n.substring(7);
                UpdateCenter.Plugin p = Hudson.getInstance().getUpdateCenter().getPlugin(n);
                if (p == null)
                    throw new Failure("No such plugin: " + n);
                p.install();
            }
        }
        rsp.sendRedirect("../updateCenter/");
    }

    public void doProxyConfigure(@QueryParameter("proxy.server") String server,
            @QueryParameter("proxy.port") String port, @QueryParameter("proxy.userName") String userName,
            @QueryParameter("proxy.password") String password, StaplerResponse rsp) throws IOException {
        Hudson.getInstance().checkPermission(Hudson.ADMINISTER);

        Hudson hudson = Hudson.getInstance();
        server = Util.fixEmptyAndTrim(server);
        if (server == null) {
            hudson.proxy = null;
            ProxyConfiguration.getXmlFile().delete();
        } else {
            hudson.proxy = new ProxyConfiguration(server, Integer.parseInt(Util.fixEmptyAndTrim(port)),
                    Util.fixEmptyAndTrim(userName), Util.fixEmptyAndTrim(password));
            hudson.proxy.save();
        }
        rsp.sendRedirect("./advanced");
    }

    /**
     * Uploads a plugin.
     */
    public HttpResponse doUploadPlugin(StaplerRequest req) throws IOException, ServletException {
        try {
            Hudson.getInstance().checkPermission(Hudson.ADMINISTER);

            ServletFileUpload upload = new ServletFileUpload(new DiskFileItemFactory());

            // Parse the request
            FileItem fileItem = (FileItem) upload.parseRequest(req).get(0);
            String fileName = Util.getFileName(fileItem.getName());
            if (!fileName.endsWith(".hpi"))
                throw new Failure(hudson.model.Messages.Hudson_NotAPlugin(fileName));
            fileItem.write(new File(rootDir, fileName));
            fileItem.delete();

            pluginUploaded = true;

            return new HttpRedirect(".");
        } catch (IOException e) {
            throw e;
        } catch (Exception e) {// grrr. fileItem.write throws this
            throw new ServletException(e);
        }
    }

    private final class UberClassLoader extends ClassLoader {
        public UberClassLoader() {
            super(PluginManager.class.getClassLoader());
        }

        @Override
        protected Class<?> findClass(String name) throws ClassNotFoundException {
            // first, use the context classloader so that plugins that are loading
            // can use its own classloader first.
            ClassLoader cl = Thread.currentThread().getContextClassLoader();
            if (cl != null && cl != this)
                try {
                    return cl.loadClass(name);
                } catch (ClassNotFoundException e) {
                    // not found. try next
                }

            for (PluginWrapper p : activePlugins) {
                try {
                    return p.classLoader.loadClass(name);
                } catch (ClassNotFoundException e) {
                    //not found. try next
                }
            }
            // not found in any of the classloader. delegate.
            throw new ClassNotFoundException(name);
        }

        @Override
        protected URL findResource(String name) {
            for (PluginWrapper p : activePlugins) {
                URL url = p.classLoader.getResource(name);
                if (url != null)
                    return url;
            }
            return null;
        }

        @Override
        protected Enumeration<URL> findResources(String name) throws IOException {
            List<URL> resources = new ArrayList<URL>();
            for (PluginWrapper p : activePlugins) {
                resources.addAll(Collections.list(p.classLoader.getResources(name)));
            }
            return Collections.enumeration(resources);
        }
    }

    private static final Logger LOGGER = Logger.getLogger(PluginManager.class.getName());

    /**
     * Remembers why a plugin failed to deploy.
     */
    public static final class FailedPlugin {
        public final String name;
        public final Exception cause;

        public FailedPlugin(String name, Exception cause) {
            this.name = name;
            this.cause = cause;
        }

        public String getExceptionString() {
            return Functions.printThrowable(cause);
        }
    }
}