diff --git a/core/src/main/java/hudson/ExtensionFinder.java b/core/src/main/java/hudson/ExtensionFinder.java index 5d7d13d8f4..1ff0638c10 100644 --- a/core/src/main/java/hudson/ExtensionFinder.java +++ b/core/src/main/java/hudson/ExtensionFinder.java @@ -23,6 +23,7 @@ */ package hudson; +import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; @@ -35,11 +36,13 @@ import com.google.inject.Module; import com.google.inject.Provider; import com.google.inject.Scope; import com.google.inject.Scopes; +import com.google.inject.matcher.Matchers; import com.google.inject.name.Names; -import com.google.common.collect.ImmutableList; +import com.google.inject.spi.ProvisionListener; import hudson.init.InitMilestone; import hudson.model.Descriptor; import hudson.model.Hudson; +import hudson.util.ReflectionUtils; import jenkins.ExtensionComponentSet; import jenkins.ExtensionFilter; import jenkins.ExtensionRefreshException; @@ -49,20 +52,25 @@ import net.java.sezpoz.Index; import net.java.sezpoz.IndexItem; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.springframework.util.ClassUtils; +import javax.annotation.PostConstruct; import java.lang.annotation.Annotation; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Map.Entry; -import java.util.logging.Logger; -import java.util.logging.Level; -import java.util.List; -import java.util.ArrayList; -import java.util.Collection; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; /** * Discovers the implementations of an extension point. @@ -454,7 +462,7 @@ public abstract class ExtensionFinder implements ExtensionPoint { * Instead of using SezPoz to instantiate, we'll instantiate them by using Guice, * so that we can take advantage of dependency injection. */ - private class SezpozModule extends AbstractModule { + private class SezpozModule extends AbstractModule implements ProvisionListener { private final List> index; private final List> loadedIndex; @@ -503,6 +511,9 @@ public abstract class ExtensionFinder implements ExtensionPoint { @SuppressWarnings({"unchecked", "ChainOfInstanceofChecks"}) @Override protected void configure() { + + bindListener(Matchers.any(), this); + for (final IndexItem item : index) { boolean optional = isOptional(item.annotation()); try { @@ -550,9 +561,64 @@ public abstract class ExtensionFinder implements ExtensionPoint { public List> getLoadedIndex() { return Collections.unmodifiableList(loadedIndex); } + + @Override + public void onProvision(ProvisionInvocation provision) { + final T instance = provision.provision(); + if (instance == null) return; + List methods = new LinkedList<>(); + Class c = instance.getClass(); + + // find PostConstruct methods in class hierarchy, the one from parent class being first in list + // so that we invoke them before derived class one. This isn't specified in JSR-250 but implemented + // this way in Spring and what most developers would expect to happen. + + final Set interfaces = ClassUtils.getAllInterfacesAsSet(instance); + + while (c != Object.class) { + Arrays.stream(c.getDeclaredMethods()) + .map(m -> getMethodAndInterfaceDeclarations(m, interfaces)) + .flatMap(Collection::stream) + .filter(m -> m.getAnnotation(PostConstruct.class) != null) + .findFirst() + .ifPresent(method -> methods.add(0, method)); + c = c.getSuperclass(); + } + + for (Method postConstruct : methods) { + try { + postConstruct.setAccessible(true); + postConstruct.invoke(instance); + } catch (final Exception e) { + throw new RuntimeException(String.format("@PostConstruct %s", postConstruct), e); + } + } + } } } + /** + * Returns initial {@link Method} as well as all matching ones found in interfaces. + * This allows to introspect metadata for a method which is both declared in parent class and in implemented + * interface(s). interfaces typically is obtained by {@link ClassUtils#getAllInterfacesAsSet} + */ + Collection getMethodAndInterfaceDeclarations(Method method, Collection interfaces) { + final List methods = new ArrayList<>(); + methods.add(method); + + // we search for matching method by iteration and comparison vs getMethod to avoid repeated NoSuchMethodException + // being thrown, while interface typically only define a few set of methods to check. + interfaces.stream() + .map(Class::getMethods) + .flatMap(Arrays::stream) + .filter(m -> m.getName().equals(method.getName()) && Arrays.equals(m.getParameterTypes(), method.getParameterTypes())) + .findFirst() + .ifPresent(methods::add); + + return methods; + } + + /** * The bootstrap implementation that looks for the {@link Extension} marker. * diff --git a/core/src/main/java/hudson/model/Descriptor.java b/core/src/main/java/hudson/model/Descriptor.java index aef728d373..f8e4c2cfeb 100644 --- a/core/src/main/java/hudson/model/Descriptor.java +++ b/core/src/main/java/hudson/model/Descriptor.java @@ -51,6 +51,8 @@ import org.apache.commons.io.IOUtils; import static hudson.util.QuotedStringTokenizer.*; import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; + +import javax.annotation.PostConstruct; import javax.servlet.ServletException; import javax.servlet.RequestDispatcher; import java.io.File; @@ -117,6 +119,8 @@ import javax.annotation.Nullable; * {@link Descriptor} can persist data just by storing them in fields. * However, it is the responsibility of the derived type to properly * invoke {@link #save()} and {@link #load()}. + * {@link #load()} is automatically invoked as a JSR-250 lifecycle method if derived class + * do implement {@link PersistentDescriptor}. * *

Reflection Enhancement

* {@link Descriptor} defines addition to the standard Java reflection diff --git a/core/src/main/java/hudson/model/PersistentDescriptor.java b/core/src/main/java/hudson/model/PersistentDescriptor.java new file mode 100644 index 0000000000..837d154cfb --- /dev/null +++ b/core/src/main/java/hudson/model/PersistentDescriptor.java @@ -0,0 +1,16 @@ +package hudson.model; + +import javax.annotation.PostConstruct; + +/** + * Marker interface for Descriptors which use xml persistent data, and as such need to load from disk when instantiated. + *

+ * {@link Descriptor#load()} method is annotated as {@link PostConstruct} so it get automatically invoked after + * constructor and field injection. + * @author Nicolas De Loof + */ +public interface PersistentDescriptor extends Saveable { + + @PostConstruct + void load(); +} diff --git a/core/src/main/java/hudson/model/UsageStatistics.java b/core/src/main/java/hudson/model/UsageStatistics.java index 2f78840b63..bb791d38e1 100644 --- a/core/src/main/java/hudson/model/UsageStatistics.java +++ b/core/src/main/java/hudson/model/UsageStatistics.java @@ -66,7 +66,7 @@ import jenkins.util.SystemProperties; * @author Kohsuke Kawaguchi */ @Extension -public class UsageStatistics extends PageDecorator { +public class UsageStatistics extends PageDecorator implements PersistentDescriptor { private final String keyImage; /** @@ -88,7 +88,6 @@ public class UsageStatistics extends PageDecorator { */ public UsageStatistics(String keyImage) { this.keyImage = keyImage; - load(); } /** diff --git a/core/src/main/java/hudson/security/csrf/DefaultCrumbIssuer.java b/core/src/main/java/hudson/security/csrf/DefaultCrumbIssuer.java index 7f833c17f3..ae4a4edfab 100644 --- a/core/src/main/java/hudson/security/csrf/DefaultCrumbIssuer.java +++ b/core/src/main/java/hudson/security/csrf/DefaultCrumbIssuer.java @@ -12,6 +12,7 @@ import java.util.logging.Level; import java.util.logging.Logger; import hudson.Extension; +import hudson.model.PersistentDescriptor; import jenkins.util.SystemProperties; import hudson.Util; import jenkins.model.Jenkins; @@ -121,13 +122,12 @@ public class DefaultCrumbIssuer extends CrumbIssuer { } @Extension @Symbol("standard") - public static final class DescriptorImpl extends CrumbIssuerDescriptor implements ModelObject { + public static final class DescriptorImpl extends CrumbIssuerDescriptor implements ModelObject, PersistentDescriptor { private final static HexStringConfidentialKey CRUMB_SALT = new HexStringConfidentialKey(Jenkins.class,"crumbSalt",16); public DescriptorImpl() { super(CRUMB_SALT.get(), SystemProperties.getString("hudson.security.csrf.requestfield", CrumbIssuer.DEFAULT_CRUMB_NAME)); - load(); } @Override diff --git a/core/src/main/java/hudson/tasks/Maven.java b/core/src/main/java/hudson/tasks/Maven.java index 910e3a6ebf..ac36f7fcde 100644 --- a/core/src/main/java/hudson/tasks/Maven.java +++ b/core/src/main/java/hudson/tasks/Maven.java @@ -24,6 +24,7 @@ package hudson.tasks; import hudson.Extension; +import hudson.model.PersistentDescriptor; import jenkins.MasterToSlaveFileCallable; import hudson.Launcher; import hudson.Functions; @@ -424,13 +425,12 @@ public class Maven extends Builder { public static DescriptorImpl DESCRIPTOR; @Extension @Symbol("maven") - public static final class DescriptorImpl extends BuildStepDescriptor { + public static final class DescriptorImpl extends BuildStepDescriptor implements PersistentDescriptor { @CopyOnWrite private volatile MavenInstallation[] installations = new MavenInstallation[0]; public DescriptorImpl() { DESCRIPTOR = this; - load(); } public boolean isApplicable(Class jobType) { diff --git a/core/src/main/java/hudson/tasks/Shell.java b/core/src/main/java/hudson/tasks/Shell.java index 4360f4ce94..3455076d5b 100644 --- a/core/src/main/java/hudson/tasks/Shell.java +++ b/core/src/main/java/hudson/tasks/Shell.java @@ -27,6 +27,7 @@ import hudson.FilePath; import hudson.Util; import hudson.Extension; import hudson.model.AbstractProject; +import hudson.model.PersistentDescriptor; import hudson.remoting.VirtualChannel; import hudson.util.FormValidation; import java.io.IOException; @@ -131,16 +132,12 @@ public class Shell extends CommandInterpreter { } @Extension @Symbol("shell") - public static class DescriptorImpl extends BuildStepDescriptor { + public static class DescriptorImpl extends BuildStepDescriptor implements PersistentDescriptor { /** * Shell executable, or null to default. */ private String shell; - public DescriptorImpl() { - load(); - } - public boolean isApplicable(Class jobType) { return true; } diff --git a/core/src/main/java/hudson/triggers/SCMTrigger.java b/core/src/main/java/hudson/triggers/SCMTrigger.java index 7afeeacdb0..83a6383d82 100644 --- a/core/src/main/java/hudson/triggers/SCMTrigger.java +++ b/core/src/main/java/hudson/triggers/SCMTrigger.java @@ -37,6 +37,7 @@ import hudson.model.AdministrativeMonitor; import hudson.model.Cause; import hudson.model.CauseAction; import hudson.model.Item; +import hudson.model.PersistentDescriptor; import hudson.model.Run; import hudson.scm.SCM; import hudson.scm.SCMDescriptor; @@ -84,6 +85,8 @@ import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import org.kohsuke.stapler.StaplerResponse; +import javax.annotation.PostConstruct; + import static java.util.logging.Level.WARNING; @@ -211,7 +214,7 @@ public class SCMTrigger extends Trigger { } @Extension @Symbol("pollSCM") - public static class DescriptorImpl extends TriggerDescriptor { + public static class DescriptorImpl extends TriggerDescriptor implements PersistentDescriptor { private static ThreadFactory threadFactory() { return new NamingThreadFactory(Executors.defaultThreadFactory(), "SCMTrigger"); @@ -242,11 +245,6 @@ public class SCMTrigger extends Trigger { private static final int THREADS_UPPER_BOUND = 100; private static final int THREADS_DEFAULT= 10; - public DescriptorImpl() { - load(); - resizeThreadPool(); - } - private void readResolve() { if (maximumThreads == 0) { maximumThreads = THREADS_DEFAULT; @@ -347,6 +345,7 @@ public class SCMTrigger extends Trigger { /** * Update the {@link ExecutorService} instance. */ + @PostConstruct /*package*/ synchronized void resizeThreadPool() { queue.setExecutors(Executors.newFixedThreadPool(maximumThreads, threadFactory())); } diff --git a/core/src/main/java/jenkins/CLI.java b/core/src/main/java/jenkins/CLI.java index 4d9f9af3df..e0a7f38957 100644 --- a/core/src/main/java/jenkins/CLI.java +++ b/core/src/main/java/jenkins/CLI.java @@ -4,6 +4,8 @@ import hudson.Extension; import hudson.model.AdministrativeMonitor; import java.io.IOException; import javax.annotation.Nonnull; + +import hudson.model.PersistentDescriptor; import jenkins.model.GlobalConfiguration; import jenkins.model.GlobalConfigurationCategory; import org.jenkinsci.Symbol; @@ -23,7 +25,7 @@ import org.kohsuke.stapler.interceptor.RequirePOST; */ @Restricted(NoExternalUse.class) @Extension @Symbol("remotingCLI") -public class CLI extends GlobalConfiguration { +public class CLI extends GlobalConfiguration implements PersistentDescriptor { /** * Supersedes {@link #isEnabled} if set. @@ -39,10 +41,6 @@ public class CLI extends GlobalConfiguration { private boolean enabled = true; // historical default, but overridden in SetupWizard - public CLI() { - load(); - } - @Override public @Nonnull GlobalConfigurationCategory getCategory() { return GlobalConfigurationCategory.get(GlobalConfigurationCategory.Security.class); diff --git a/core/src/main/java/jenkins/model/ArtifactManagerConfiguration.java b/core/src/main/java/jenkins/model/ArtifactManagerConfiguration.java index 29d7439d03..cb411800d9 100644 --- a/core/src/main/java/jenkins/model/ArtifactManagerConfiguration.java +++ b/core/src/main/java/jenkins/model/ArtifactManagerConfiguration.java @@ -25,6 +25,7 @@ package jenkins.model; import hudson.Extension; +import hudson.model.PersistentDescriptor; import hudson.util.DescribableList; import java.io.IOException; import net.sf.json.JSONObject; @@ -38,7 +39,7 @@ import javax.annotation.Nonnull; * @since 1.532 */ @Extension @Symbol("artifactManager") -public class ArtifactManagerConfiguration extends GlobalConfiguration { +public class ArtifactManagerConfiguration extends GlobalConfiguration implements PersistentDescriptor { public static @Nonnull ArtifactManagerConfiguration get() { return GlobalConfiguration.all().getInstance(ArtifactManagerConfiguration.class); @@ -46,10 +47,6 @@ public class ArtifactManagerConfiguration extends GlobalConfiguration { private final DescribableList artifactManagerFactories = new DescribableList(this); - public ArtifactManagerConfiguration() { - load(); - } - private Object readResolve() { artifactManagerFactories.setOwner(this); return this; diff --git a/core/src/main/java/jenkins/model/DownloadSettings.java b/core/src/main/java/jenkins/model/DownloadSettings.java index 8c8b75b90b..6f5e6aeabb 100644 --- a/core/src/main/java/jenkins/model/DownloadSettings.java +++ b/core/src/main/java/jenkins/model/DownloadSettings.java @@ -30,6 +30,7 @@ import hudson.model.AdministrativeMonitor; import hudson.model.AsyncPeriodicWork; import hudson.model.DownloadService; import hudson.model.DownloadService.Downloadable; +import hudson.model.PersistentDescriptor; import hudson.model.TaskListener; import hudson.model.UpdateSite; import hudson.util.FormValidation; @@ -51,7 +52,7 @@ import javax.annotation.Nonnull; */ @Restricted(NoExternalUse.class) // no clear reason for this to be an API @Extension @Symbol("downloadSettings") -public final class DownloadSettings extends GlobalConfiguration { +public final class DownloadSettings extends GlobalConfiguration implements PersistentDescriptor { public static @Nonnull DownloadSettings get() { return GlobalConfiguration.all().getInstance(DownloadSettings.class); @@ -59,10 +60,6 @@ public final class DownloadSettings extends GlobalConfiguration { private boolean useBrowser = false; - public DownloadSettings() { - load(); - } - public boolean isUseBrowser() { return useBrowser; } diff --git a/core/src/main/java/jenkins/model/JenkinsLocationConfiguration.java b/core/src/main/java/jenkins/model/JenkinsLocationConfiguration.java index ed402938b4..1e9213df75 100644 --- a/core/src/main/java/jenkins/model/JenkinsLocationConfiguration.java +++ b/core/src/main/java/jenkins/model/JenkinsLocationConfiguration.java @@ -3,6 +3,7 @@ package jenkins.model; import hudson.Extension; import hudson.Util; import hudson.XmlFile; +import hudson.model.PersistentDescriptor; import hudson.util.FormValidation; import hudson.util.XStream2; import org.jenkinsci.Symbol; @@ -31,7 +32,7 @@ import javax.annotation.Nonnull; * @since 1.494 */ @Extension @Symbol("location") -public class JenkinsLocationConfiguration extends GlobalConfiguration { +public class JenkinsLocationConfiguration extends GlobalConfiguration implements PersistentDescriptor { /** * @deprecated replaced by {@link #jenkinsUrl} */ @@ -59,10 +60,6 @@ public class JenkinsLocationConfiguration extends GlobalConfiguration { return config; } - public JenkinsLocationConfiguration() { - load(); - } - @Override public synchronized void load() { // for backward compatibility, if we don't have our own data yet, then diff --git a/core/src/main/java/jenkins/mvn/GlobalMavenConfig.java b/core/src/main/java/jenkins/mvn/GlobalMavenConfig.java index 24cdb171e2..477aa34afd 100644 --- a/core/src/main/java/jenkins/mvn/GlobalMavenConfig.java +++ b/core/src/main/java/jenkins/mvn/GlobalMavenConfig.java @@ -1,6 +1,7 @@ package jenkins.mvn; import hudson.Extension; +import hudson.model.PersistentDescriptor; import jenkins.model.GlobalConfiguration; import jenkins.model.GlobalConfigurationCategory; import jenkins.tools.ToolConfigurationCategory; @@ -11,14 +12,10 @@ import javax.annotation.Nonnull; //as close as it gets to the global Maven Project configuration @Extension(ordinal = 50) @Symbol("maven") -public class GlobalMavenConfig extends GlobalConfiguration { +public class GlobalMavenConfig extends GlobalConfiguration implements PersistentDescriptor { private SettingsProvider settingsProvider; private GlobalSettingsProvider globalSettingsProvider; - public GlobalMavenConfig() { - load(); - } - @Override public @Nonnull ToolConfigurationCategory getCategory() { return GlobalConfigurationCategory.get(ToolConfigurationCategory.class); diff --git a/core/src/main/java/jenkins/security/QueueItemAuthenticatorConfiguration.java b/core/src/main/java/jenkins/security/QueueItemAuthenticatorConfiguration.java index df85943f26..c6283401b2 100644 --- a/core/src/main/java/jenkins/security/QueueItemAuthenticatorConfiguration.java +++ b/core/src/main/java/jenkins/security/QueueItemAuthenticatorConfiguration.java @@ -1,6 +1,7 @@ package jenkins.security; import hudson.Extension; +import hudson.model.PersistentDescriptor; import hudson.model.queue.Tasks; import hudson.util.DescribableList; import jenkins.model.GlobalConfiguration; @@ -21,14 +22,10 @@ import java.util.List; * @since 1.520 */ @Extension @Symbol("queueItemAuthenticator") -public class QueueItemAuthenticatorConfiguration extends GlobalConfiguration { +public class QueueItemAuthenticatorConfiguration extends GlobalConfiguration implements PersistentDescriptor { private final DescribableList authenticators = new DescribableList(this); - public QueueItemAuthenticatorConfiguration() { - load(); - } - private Object readResolve() { authenticators.setOwner(this); return this; diff --git a/core/src/main/java/jenkins/security/UpdateSiteWarningsConfiguration.java b/core/src/main/java/jenkins/security/UpdateSiteWarningsConfiguration.java index 8ea916bab4..a623702258 100644 --- a/core/src/main/java/jenkins/security/UpdateSiteWarningsConfiguration.java +++ b/core/src/main/java/jenkins/security/UpdateSiteWarningsConfiguration.java @@ -26,6 +26,7 @@ package jenkins.security; import hudson.Extension; import hudson.PluginWrapper; +import hudson.model.PersistentDescriptor; import hudson.model.UpdateSite; import jenkins.model.GlobalConfiguration; import jenkins.model.GlobalConfigurationCategory; @@ -50,7 +51,7 @@ import java.util.Set; */ @Extension @Restricted(NoExternalUse.class) -public class UpdateSiteWarningsConfiguration extends GlobalConfiguration { +public class UpdateSiteWarningsConfiguration extends GlobalConfiguration implements PersistentDescriptor { private HashSet ignoredWarnings = new HashSet<>(); @@ -59,10 +60,6 @@ public class UpdateSiteWarningsConfiguration extends GlobalConfiguration { return GlobalConfigurationCategory.get(GlobalConfigurationCategory.Security.class); } - public UpdateSiteWarningsConfiguration() { - load(); - } - @Nonnull public Set getIgnoredWarnings() { return Collections.unmodifiableSet(ignoredWarnings); diff --git a/core/src/main/java/jenkins/security/apitoken/ApiTokenPropertyConfiguration.java b/core/src/main/java/jenkins/security/apitoken/ApiTokenPropertyConfiguration.java index ba7c866d73..e032f2818d 100644 --- a/core/src/main/java/jenkins/security/apitoken/ApiTokenPropertyConfiguration.java +++ b/core/src/main/java/jenkins/security/apitoken/ApiTokenPropertyConfiguration.java @@ -24,6 +24,7 @@ package jenkins.security.apitoken; import hudson.Extension; +import hudson.model.PersistentDescriptor; import jenkins.model.GlobalConfiguration; import jenkins.model.GlobalConfigurationCategory; import org.jenkinsci.Symbol; @@ -35,7 +36,7 @@ import org.jenkinsci.Symbol; */ @Extension @Symbol("apiToken") -public class ApiTokenPropertyConfiguration extends GlobalConfiguration { +public class ApiTokenPropertyConfiguration extends GlobalConfiguration implements PersistentDescriptor { /** * When a user is created, this property determines whether or not we create a legacy token for the user. * For security reasons, we do not recommend you enable this but we left that open to ease upgrades. @@ -58,10 +59,6 @@ public class ApiTokenPropertyConfiguration extends GlobalConfiguration { return GlobalConfiguration.all().get(ApiTokenPropertyConfiguration.class); } - public ApiTokenPropertyConfiguration() { - load(); - } - public boolean hasExistingConfigFile(){ return getConfigFile().exists(); }