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:
+ *
+ * - {@link LocalPluginManager#LocalPluginManager(Jenkins)}
+ * - {@link LocalPluginManager#LocalPluginManager(ServletContext, File)}
+ * - {@link LocalPluginManager#LocalPluginManager(File)}
+ *
+ * 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 extends PluginManager> klass,
+ @NonNull Jenkins jenkins) throws ReflectiveOperationException {
+ return klass.getConstructor(Jenkins.class).newInstance(jenkins);
+ }
+ },
+ SC_FILE {
+ @Override
+ @NonNull PluginManager doCreate(@NonNull Class extends PluginManager> 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 extends PluginManager> klass,
+ @NonNull Jenkins jenkins) throws ReflectiveOperationException {
+ return klass.getConstructor(File.class).newInstance(jenkins.getRootDir());
+ }
+ };
+
+ final @CheckForNull PluginManager create(@NonNull Class extends PluginManager> 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 extends PluginManager> 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 extends PluginManager> 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 extends LocalPluginManager> 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 extends CustomPluginManager> 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);
+ }
+ }
+
+}