diff --git a/core/src/main/java/hudson/LocalPluginManager.java b/core/src/main/java/hudson/LocalPluginManager.java index bc77db85a4..25e48c053d 100644 --- a/core/src/main/java/hudson/LocalPluginManager.java +++ b/core/src/main/java/hudson/LocalPluginManager.java @@ -24,26 +24,46 @@ package hudson; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; import jenkins.util.SystemProperties; import jenkins.model.Jenkins; +import javax.servlet.ServletContext; import java.io.File; import java.util.Collection; import java.util.Collections; import java.util.logging.Logger; /** - * {@link PluginManager} + * Default implementation of {@link PluginManager}. * * @author Kohsuke Kawaguchi */ public class LocalPluginManager extends PluginManager { - public LocalPluginManager(Jenkins jenkins) { - super(jenkins.servletContext, new File(jenkins.getRootDir(),"plugins")); + /** + * Creates a new LocalPluginManager + * @param context Servlet context. Provided for compatibility as {@code Jenkins.getInstance().servletContext} should be used. + * @param rootDir Jenkins home directory. + */ + public LocalPluginManager(@CheckForNull ServletContext context, @NonNull File rootDir) { + super(context, new File(rootDir,"plugins")); } - public LocalPluginManager(File rootDir) { - super(null, new File(rootDir,"plugins")); + /** + * Creates a new LocalPluginManager + * @param jenkins Jenkins instance that will use the plugin manager. + */ + public LocalPluginManager(@NonNull Jenkins jenkins) { + this(jenkins.servletContext, jenkins.getRootDir()); + } + + /** + * Creates a new LocalPluginManager + * @param rootDir Jenkins home directory. + */ + public LocalPluginManager(@NonNull File rootDir) { + this(null, rootDir); } /** diff --git a/core/src/main/java/hudson/PluginManager.java b/core/src/main/java/hudson/PluginManager.java index 8b83619253..565d800187 100644 --- a/core/src/main/java/hudson/PluginManager.java +++ b/core/src/main/java/hudson/PluginManager.java @@ -23,6 +23,7 @@ */ package hudson; +import edu.umd.cs.findbugs.annotations.NonNull; import jenkins.util.SystemProperties; import hudson.PluginWrapper.Dependency; import hudson.init.InitMilestone; @@ -136,6 +137,7 @@ import static hudson.init.InitMilestone.*; import hudson.model.DownloadService; import hudson.util.FormValidation; +import static java.util.logging.Level.FINE; import static java.util.logging.Level.INFO; import static java.util.logging.Level.SEVERE; import static java.util.logging.Level.WARNING; @@ -145,10 +147,101 @@ import org.kohsuke.accmod.restrictions.NoExternalUse; /** * Manages {@link PluginWrapper}s. * + *

+ * Setting default Plugin Managers. The default plugin manager in {@code Jenkins} can be replaced by defining a + * System Property (hudson.PluginManager.className). See {@link #createDefault(Jenkins)}. + * This className should be available on early startup, so it cannot come only from a library + * (e.g. Jenkins module or Extra library dependency in the WAR file project). + * Plugins cannot be used for such purpose. + * In order to be correctly instantiated, the class definition must have at least one constructor with the same + * signature as the following ones: + *

    + *
  1. {@link LocalPluginManager#LocalPluginManager(Jenkins)}
  2. + *
  3. {@link LocalPluginManager#LocalPluginManager(ServletContext, File)}
  4. + *
  5. {@link LocalPluginManager#LocalPluginManager(File)}
  6. + *
+ * Constructors are searched in the order provided above and only the first found suitable constructor is + * tried to build an instance. In the last two cases the {@link File} argument refers to the Jenkins home directory. + * * @author Kohsuke Kawaguchi */ @ExportedBean public abstract class PluginManager extends AbstractModelObject implements OnMaster, StaplerOverridable { + /** Custom plugin manager system property or context param. */ + public static final String CUSTOM_PLUGIN_MANAGER = PluginManager.class.getName() + ".className"; + + /** Accepted constructors for custom plugin manager, in the order they are tried. */ + private enum PMConstructor { + JENKINS { + @Override + @NonNull PluginManager doCreate(@NonNull Class klass, + @NonNull Jenkins jenkins) throws ReflectiveOperationException { + return klass.getConstructor(Jenkins.class).newInstance(jenkins); + } + }, + SC_FILE { + @Override + @NonNull PluginManager doCreate(@NonNull Class klass, + @NonNull Jenkins jenkins) throws ReflectiveOperationException { + return klass.getConstructor(ServletContext.class, File.class).newInstance(jenkins.servletContext, jenkins.getRootDir()); + } + }, + FILE { + @Override + @NonNull PluginManager doCreate(@NonNull Class klass, + @NonNull Jenkins jenkins) throws ReflectiveOperationException { + return klass.getConstructor(File.class).newInstance(jenkins.getRootDir()); + } + }; + + final @CheckForNull PluginManager create(@NonNull Class klass, + @NonNull Jenkins jenkins) throws ReflectiveOperationException { + try { + return doCreate(klass, jenkins); + } catch(NoSuchMethodException e) { + // Constructor not found. Will try the remaining ones. + return null; + } + } + + abstract @NonNull PluginManager doCreate(@NonNull Class klass, + @NonNull Jenkins jenkins) throws ReflectiveOperationException; + } + + /** + * Creates the {@link PluginManager} to use if no one is provided to a {@link Jenkins} object. + * This method will be called after creation of {@link Jenkins} object, but before it is fully initialized. + * @param jenkins Jenkins Instance. + * @return Plugin manager to use. If no custom class is configured or in case of any error, the default + * {@link LocalPluginManager} is returned. + */ + public static @NonNull PluginManager createDefault(@NonNull Jenkins jenkins) { + String pmClassName = SystemProperties.getString(CUSTOM_PLUGIN_MANAGER); + if (!StringUtils.isBlank(pmClassName)) { + LOGGER.log(FINE, String.format("Use of custom plugin manager [%s] requested.", pmClassName)); + try { + final Class klass = Class.forName(pmClassName).asSubclass(PluginManager.class); + // Iteration is in declaration order + for (PMConstructor c : PMConstructor.values()) { + PluginManager pm = c.create(klass, jenkins); + if (pm != null) { + return pm; + } + } + LOGGER.log(WARNING, String.format("Provided custom plugin manager [%s] does not provide any of the suitable constructors. Using default.", pmClassName)); + } catch(NullPointerException e) { + // Class.forName and Class.getConstructor are supposed to never return null though a broken ClassLoader + // could break the contract. Just in case we introduce this specific catch to avoid polluting the logs with NPEs. + LOGGER.log(WARNING, String.format("Unable to instantiate custom plugin manager [%s]. Using default.", pmClassName)); + } catch(ClassCastException e) { + LOGGER.log(WARNING, String.format("Provided class [%s] does not extend PluginManager. Using default.", pmClassName)); + } catch(Exception e) { + LOGGER.log(WARNING, String.format("Unable to instantiate custom plugin manager [%s]. Using default.", pmClassName), e); + } + } + return new LocalPluginManager(jenkins); + } + /** * All discovered plugins. */ @@ -218,11 +311,8 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas this.rootDir = rootDir; if(!rootDir.exists()) rootDir.mkdirs(); - String workDir = System.getProperty(PluginManager.class.getName()+".workDir"); - if (workDir == null && context != null) { - workDir = context.getInitParameter(PluginManager.class.getName() + ".workDir"); - } - this.workDir = workDir == null ? null : new File(workDir); + String workDir = SystemProperties.getString(PluginManager.class.getName()+".workDir"); + this.workDir = StringUtils.isBlank(workDir) ? null : new File(workDir); strategy = createPluginStrategy(); @@ -561,7 +651,7 @@ public abstract class PluginManager extends AbstractModelObject implements OnMas if (dependencyURL != null) { // And transitive deps... addDependencies(dependencyURL, fromPath, dependencySet); - // And then add the current plugin + // And then add the current plugin dependencySet.add(dependencyURL); } } diff --git a/core/src/main/java/jenkins/model/Jenkins.java b/core/src/main/java/jenkins/model/Jenkins.java index 2c7e8b2fd3..d0c2d9b6ee 100644 --- a/core/src/main/java/jenkins/model/Jenkins.java +++ b/core/src/main/java/jenkins/model/Jenkins.java @@ -865,7 +865,7 @@ public class Jenkins extends AbstractCIBase implements DirectlyModifiableTopLeve } if (pluginManager==null) - pluginManager = new LocalPluginManager(this); + pluginManager = PluginManager.createDefault(this); this.pluginManager = pluginManager; // JSON binding needs to be able to see all the classes from all the plugins WebApp.get(servletContext).setClassLoader(pluginManager.uberClassLoader); diff --git a/test/src/test/java/hudson/CustomPluginManagerTest.java b/test/src/test/java/hudson/CustomPluginManagerTest.java new file mode 100644 index 0000000000..fa84da86f4 --- /dev/null +++ b/test/src/test/java/hudson/CustomPluginManagerTest.java @@ -0,0 +1,137 @@ +/* + * The MIT License + * + * Copyright (c) 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 hudson; + +import jenkins.model.Jenkins; +import org.junit.Rule; +import org.junit.Test; +import org.jvnet.hudson.test.Issue; +import org.jvnet.hudson.test.JenkinsRecipe; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.recipes.WithPlugin; + +import javax.servlet.ServletContext; +import java.io.File; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +/** + * Tests for the use of a custom plugin manager in custom wars. + */ +public class CustomPluginManagerTest { + @Rule public final JenkinsRule r = new JenkinsRule(); + + // TODO: Move to jenkins-test-harness + @JenkinsRecipe(WithCustomLocalPluginManager.RuleRunnerImpl.class) + @Target(METHOD) + @Retention(RUNTIME) + public @interface WithCustomLocalPluginManager { + Class value(); + + class RuleRunnerImpl extends JenkinsRecipe.Runner { + private String oldValue; + + @Override + public void setup(JenkinsRule jenkinsRule, WithCustomLocalPluginManager recipe) throws Exception { + jenkinsRule.useLocalPluginManager = true; + oldValue = System.getProperty(LocalPluginManager.CUSTOM_PLUGIN_MANAGER); + System.setProperty(LocalPluginManager.CUSTOM_PLUGIN_MANAGER, recipe.value().getName()); + + } + + @Override + public void tearDown(JenkinsRule jenkinsRule, WithCustomLocalPluginManager recipe) throws Exception { + System.setProperty(LocalPluginManager.CUSTOM_PLUGIN_MANAGER, oldValue); + } + } + } + + private void check(Class klass) { + assertTrue("Correct plugin manager installed", klass.isAssignableFrom(r.getPluginManager().getClass())); + assertNotNull("Plugin 'tasks' installed", r.jenkins.getPlugin("tasks")); + } + + // An interface not to override every constructor. + interface CustomPluginManager { + } + + @Issue("JENKINS-34681") + @WithPlugin("tasks.jpi") + @WithCustomLocalPluginManager(CustomPluginManager1.class) + @Test public void customPluginManager1() { + check(CustomPluginManager1.class); + } + + public static class CustomPluginManager1 extends LocalPluginManager implements CustomPluginManager { + public CustomPluginManager1(Jenkins jenkins) { + super(jenkins); + } + } + + @Issue("JENKINS-34681") + @WithPlugin("tasks.jpi") + @WithCustomLocalPluginManager(CustomPluginManager2.class) + @Test public void customPluginManager2() { + check(CustomPluginManager2.class); + } + + public static class CustomPluginManager2 extends LocalPluginManager implements CustomPluginManager { + public CustomPluginManager2(ServletContext ctx, File root) { + super(ctx, root); + } + } + + @Issue("JENKINS-34681") + @WithPlugin("tasks.jpi") + @WithCustomLocalPluginManager(CustomPluginManager3.class) + @Test public void customPluginManager3() { + check(CustomPluginManager3.class); + } + + public static class CustomPluginManager3 extends LocalPluginManager implements CustomPluginManager { + public CustomPluginManager3(File root) { + super(root); + } + } + + @Issue("JENKINS-34681") + @WithPlugin("tasks.jpi") + @WithCustomLocalPluginManager(BadCustomPluginManager.class) + @Test public void badCustomPluginManager() { + assertFalse("Custom plugin manager not installed", r.getPluginManager() instanceof CustomPluginManager); + } + + public static class BadCustomPluginManager extends LocalPluginManager implements CustomPluginManager { + public BadCustomPluginManager(File root, ServletContext ctx) { + super(ctx, root); + } + } + +}