diff --git a/step-framework-core/src/main/java/step/core/plugins/AbstractPlugin.java b/step-framework-core/src/main/java/step/core/plugins/AbstractPlugin.java index dab6ee4..ccfba18 100644 --- a/step-framework-core/src/main/java/step/core/plugins/AbstractPlugin.java +++ b/step-framework-core/src/main/java/step/core/plugins/AbstractPlugin.java @@ -24,11 +24,6 @@ public AbstractPlugin() { super(); } - @Override - public boolean validate() { - return true; - } - @Override public String toString() { return this.getClass().getSimpleName(); diff --git a/step-framework-core/src/main/java/step/core/plugins/OptionalPlugin.java b/step-framework-core/src/main/java/step/core/plugins/OptionalPlugin.java index d856946..cc8409c 100644 --- a/step-framework-core/src/main/java/step/core/plugins/OptionalPlugin.java +++ b/step-framework-core/src/main/java/step/core/plugins/OptionalPlugin.java @@ -20,5 +20,7 @@ public interface OptionalPlugin { - public boolean validate(); + default boolean validate() { + return true; + } } diff --git a/step-framework-core/src/main/java/step/core/plugins/PluginManager.java b/step-framework-core/src/main/java/step/core/plugins/PluginManager.java index aadc99c..8120a2a 100644 --- a/step-framework-core/src/main/java/step/core/plugins/PluginManager.java +++ b/step-framework-core/src/main/java/step/core/plugins/PluginManager.java @@ -118,6 +118,10 @@ public Builder withPlugins(List plugins_) { return this; } + public List getPlugins() { + return plugins; + } + public Builder withPluginsFromClasspath() throws InstantiationException, IllegalAccessException, ClassNotFoundException { return withPluginsFromClasspath(null); } diff --git a/step-framework-server-plugins/src/main/java/step/versionmanager/VersionManagerPlugin.java b/step-framework-server-plugins/src/main/java/step/versionmanager/VersionManagerPlugin.java index cfe55ad..382efb7 100644 --- a/step-framework-server-plugins/src/main/java/step/versionmanager/VersionManagerPlugin.java +++ b/step-framework-server-plugins/src/main/java/step/versionmanager/VersionManagerPlugin.java @@ -18,9 +18,6 @@ ******************************************************************************/ package step.versionmanager; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import step.core.AbstractContext; import step.core.plugins.Plugin; import step.framework.server.ServerPlugin; @@ -28,39 +25,12 @@ @Plugin() public class VersionManagerPlugin implements ServerPlugin { - private static final Logger logger = LoggerFactory.getLogger(VersionManagerPlugin.class); - @Override - public void serverStart(C context) throws Exception { - VersionManager versionManager = new VersionManager(context); + public void init(C context) throws Exception { + VersionManager versionManager = new VersionManager<>(context); context.put(VersionManager.class, versionManager); versionManager.readLatestControllerLog(); versionManager.insertControllerLog(); } - - @Override - public void migrateData(C context) throws Exception { - - } - - @Override - public void initializeData(C context) throws Exception { - - } - - @Override - public void afterInitializeData(C context) throws Exception { - - } - - @Override - public void serverStop(C context) { - - } - - @Override - public boolean canBeDisabled() { - return true; - } } diff --git a/step-framework-server/src/main/java/step/framework/server/ControllerInitializationPlugin.java b/step-framework-server/src/main/java/step/framework/server/ControllerInitializationPlugin.java deleted file mode 100644 index fc364a1..0000000 --- a/step-framework-server/src/main/java/step/framework/server/ControllerInitializationPlugin.java +++ /dev/null @@ -1,37 +0,0 @@ -/******************************************************************************* - * Copyright (C) 2020, exense GmbH - * - * This file is part of STEP - * - * STEP is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * STEP is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with STEP. If not, see . - ******************************************************************************/ -package step.framework.server; - - -import step.core.AbstractContext; - -public interface ControllerInitializationPlugin { - - public void checkPreconditions(C context) throws Exception; - - public void init(C context) throws Exception; - - public void recover(C context) throws Exception; - - public void finalizeStart(C context) throws Exception; - - public void preShutdownHook(C context); - - public void postShutdownHook(C context); -} diff --git a/step-framework-server/src/main/java/step/framework/server/ControllerServer.java b/step-framework-server/src/main/java/step/framework/server/ControllerServer.java index b894368..d2e6219 100644 --- a/step-framework-server/src/main/java/step/framework/server/ControllerServer.java +++ b/step-framework-server/src/main/java/step/framework/server/ControllerServer.java @@ -72,19 +72,17 @@ public class ControllerServer { public static final String UI_CONTEXT_ROOT_DEFAULT_VALUE = "/"; private final String contextRoot; private final boolean defaultServlet; - private Configuration configuration; + private final Configuration configuration; private Server server; private ContextHandlerCollection handlers; - private Integer port; + private final Integer port; private static final Logger logger = LoggerFactory.getLogger(ControllerServer.class); - private ServerPlugin pluginProxy; - - ControllerInitializationPlugin initPluginProxy; + private ServerPlugin pluginProxy; private AbstractContext serverContext; @@ -161,10 +159,12 @@ private void stop() { } stopping = true; - try { - initPluginProxy.preShutdownHook(serverContext); - } catch (Exception e) { - logger.error("Error while calling plugin pre-shutdown hooks"); + if (pluginProxy != null) { + try { + pluginProxy.preShutdownHook(serverContext); + } catch (Exception e) { + logger.error("Error while calling plugin pre-shutdown hooks"); + } } try { server.stop(); @@ -192,10 +192,12 @@ private void stop() { } } - try { - initPluginProxy.postShutdownHook(serverContext); - } catch (Exception e) { - logger.error("Error while calling plugin post-shutdown hooks"); + if (pluginProxy != null) { + try { + pluginProxy.postShutdownHook(); + } catch (Exception e) { + logger.error("Error while calling plugin post-shutdown hooks"); + } } } @@ -303,24 +305,26 @@ protected void configure() { serverContext.put(ServiceRegistrationCallback.class, serviceRegistrationCallback); serverContext.put(Configuration.class, configuration); - //Initialization plugins check preconditions and recover - PluginManager initPluginManager = (new ServerPluginManager(configuration, null)) - .cloneAs(ControllerInitializationPlugin.class); - initPluginProxy = initPluginManager.getProxy(); + // Phase 1: scan classpath once with no moduleChecker, run checkPreconditions so that + // the providing plugin can register the ModuleChecker into the context + ServerPluginManager bootstrapManager = new ServerPluginManager(configuration, null); + pluginProxy = bootstrapManager.getProxy(); + logger.info("Checking preconditions..."); - initPluginProxy.checkPreconditions(serverContext); + pluginProxy.checkPreconditions(serverContext); - //module checker must be created in the checkPreconditions phase of the ControllerInitializationPlugin plugins + // Phase 2: rebuild from the already-scanned plugin list using the now-registered moduleChecker ModuleChecker moduleChecker = serverContext.get(ModuleChecker.class); - //Create plugins manager for all plugins and add it to context (required for init phases) - ServerPluginManager serverPluginManager = new ServerPluginManager(configuration, moduleChecker); + ServerPluginManager serverPluginManager = moduleChecker != null + ? bootstrapManager.rebuild(moduleChecker) + : bootstrapManager; serverContext.put(ServerPluginManager.class, serverPluginManager); pluginProxy = serverPluginManager.getProxy(); logger.info("Initializing..."); - initPluginProxy.init(serverContext); + pluginProxy.init(serverContext); logger.info("Recovering controller..."); - initPluginProxy.recover(serverContext); + pluginProxy.recover(serverContext); //start all plugins and init data logger.info("Starting controller..."); @@ -333,7 +337,7 @@ protected void configure() { pluginProxy.afterInitializeData(serverContext); //Initialization plugins cal final steps - initPluginProxy.finalizeStart(serverContext); + pluginProxy.finalizeStart(serverContext); //Http session management SessionHandler s = new SessionHandler(); diff --git a/step-framework-server/src/main/java/step/framework/server/ServerPlugin.java b/step-framework-server/src/main/java/step/framework/server/ServerPlugin.java index 0912df9..74c266f 100644 --- a/step-framework-server/src/main/java/step/framework/server/ServerPlugin.java +++ b/step-framework-server/src/main/java/step/framework/server/ServerPlugin.java @@ -20,18 +20,117 @@ import step.core.AbstractContext; +/** + * Lifecycle interface for server plugins. Implementations are discovered automatically via + * classpath scanning and invoked by the {@link ServerPluginManager} through a proxy in a + * well-defined startup/shutdown order. + * + *

The startup sequence is: + *

    + *
  1. {@link #checkPreconditions(AbstractContext)} — earliest phase; used to validate + * environment requirements or register infrastructure objects (e.g. a + * {@link step.core.plugins.ModuleChecker}) into the context before the final plugin + * set is determined.
  2. + *
  3. {@link #init(AbstractContext)} — main initialization phase.
  4. + *
  5. {@link #recover(AbstractContext)} — recovery phase, e.g. after an unclean shutdown.
  6. + *
  7. {@link #serverStart(AbstractContext)} — called once the server is ready to start + * accepting work.
  8. + *
  9. {@link #migrateData(AbstractContext)} — data migration tasks.
  10. + *
  11. {@link #initializeData(AbstractContext)} — initial data population.
  12. + *
  13. {@link #afterInitializeData(AbstractContext)} — post data initialization scripts.
  14. + *
  15. {@link #finalizeStart(AbstractContext)} — final step of the startup sequence.
  16. + *
+ * + *

The shutdown sequence is: + *

    + *
  1. {@link #preShutdownHook(AbstractContext)} — called before the server begins shutting down.
  2. + *
  3. {@link #serverStop(AbstractContext)} — called while stopping the server.
  4. + *
  5. {@link #postShutdownHook()} — called after the server has shut down.
  6. + *
+ * + *

All methods have empty default implementations so that plugins only need to override the + * phases they participate in. + * + * @param the context type passed through the lifecycle + */ public interface ServerPlugin { - public void serverStart(C context) throws Exception; + /** + * Earliest startup phase. Use this to validate preconditions or register infrastructure + * objects into the context (e.g. a {@link step.core.plugins.ModuleChecker}) that must be + * available before the final set of enabled plugins is determined. + */ + default void checkPreconditions(C context) throws Exception { + } + + /** + * Main initialization phase, called after preconditions have been checked and the active + * plugin set has been finalized. + */ + default void init(C context) throws Exception { + } + + /** + * Recovery phase, called after {@link #init(AbstractContext)}, e.g. to handle state left + * over from an unclean shutdown. + */ + default void recover(C context) throws Exception { + } + + /** + * Called once the server is ready to start accepting work. + */ + default void serverStart(C context) throws Exception { + } + + /** + * Data migration tasks, executed after {@link #serverStart(AbstractContext)}. + */ + default void migrateData(C context) throws Exception { + } + + /** + * Initial data population, executed after {@link #migrateData(AbstractContext)}. + */ + default void initializeData(C context) throws Exception { + } + + /** + * Post data initialization scripts, executed after {@link #initializeData(AbstractContext)}. + */ + default void afterInitializeData(C context) throws Exception { + } - public void migrateData(C context) throws Exception; + /** + * Final step of the startup sequence. + */ + default void finalizeStart(C context) throws Exception { + } - public void initializeData(C context) throws Exception; + /** + * Called before the server begins shutting down. + */ + default void preShutdownHook(C context) { + } - public void afterInitializeData(C context) throws Exception; + /** + * Called when stopping the controller + */ + default void serverStop(C context) { + } - public void serverStop(C context); + /** + * Called after the server has shut down. + */ + default void postShutdownHook() { + } - boolean canBeDisabled(); + /** + * Returns {@code false} if this plugin is mandatory and must not be disabled via + * configuration or module filtering. Defaults to {@code true}. + */ + default boolean canBeDisabled() { + return true; + } } diff --git a/step-framework-server/src/main/java/step/framework/server/ServerPluginManager.java b/step-framework-server/src/main/java/step/framework/server/ServerPluginManager.java index 5ab18e6..0153a0d 100644 --- a/step-framework-server/src/main/java/step/framework/server/ServerPluginManager.java +++ b/step-framework-server/src/main/java/step/framework/server/ServerPluginManager.java @@ -1,74 +1,103 @@ -/******************************************************************************* - * Copyright (C) 2020, exense GmbH - * - * This file is part of STEP - * - * STEP is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * STEP is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with STEP. If not, see . - ******************************************************************************/ -package step.framework.server; - -import ch.exense.commons.app.Configuration; -import step.core.plugins.ModuleChecker; -import step.core.plugins.PluginManager; -import step.core.plugins.PluginManager.Builder; -import step.core.plugins.PluginManager.Builder.CircularDependencyException; -import step.core.plugins.exceptions.PluginCriticalException; - -import java.util.List; -import java.util.stream.Collectors; - -public class ServerPluginManager

{ - - protected Configuration configuration; - - protected ModuleChecker moduleChecker; - - protected PluginManager pluginManager; - - public ServerPluginManager(Configuration configuration, ModuleChecker moduleChecker) throws CircularDependencyException, InstantiationException, IllegalAccessException, ClassNotFoundException { - this.configuration = configuration; - this.moduleChecker = moduleChecker; - Builder builder = new PluginManager.Builder(ServerPlugin.class); - this.pluginManager = builder.withPluginsFromClasspath().withPluginFilter(this::isPluginEnabled).build(); - } - - public ServerPlugin getProxy() { - return pluginManager.getProxy(ServerPlugin.class); - } - - protected boolean isPluginEnabled(ServerPlugin plugin) { - String pluginName = plugin.getClass().getSimpleName(); - boolean enabled = configuration.getPropertyAsBoolean("plugins." + pluginName + ".enabled", true) - && (moduleChecker == null || moduleChecker.apply(plugin)); - if (!enabled && !plugin.canBeDisabled()) { - throw new PluginCriticalException("The plugin " + pluginName + " cannot be disabled"); - } - return enabled; - } - - public Configuration getConfiguration() { - return configuration; - } - - public PluginManager getPluginManager() { - return pluginManager; - } - - - public PluginManager

cloneAs(Class

pluginClass) throws CircularDependencyException { - PluginManager.Builder

builder = new PluginManager.Builder(pluginClass); - List

collect = this.getPluginManager().getPlugins().stream().filter(pluginClass::isInstance).map(pluginClass::cast).collect(Collectors.toList()); - return builder.withPlugins(collect).build(); - } -} +/******************************************************************************* + * Copyright (C) 2020, exense GmbH + * + * This file is part of STEP + * + * STEP is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * STEP is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with STEP. If not, see . + ******************************************************************************/ +package step.framework.server; + +import ch.exense.commons.app.Configuration; +import step.core.AbstractContext; +import step.core.plugins.ModuleChecker; +import step.core.plugins.PluginManager; +import step.core.plugins.PluginManager.Builder; +import step.core.plugins.PluginManager.Builder.CircularDependencyException; +import step.core.plugins.exceptions.PluginCriticalException; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +public class ServerPluginManager { + + // ServerPlugin.class is Class due to erasure; the cast to Class> + // is safe here because we only ever use it to parameterize the PluginManager builder. + @SuppressWarnings("unchecked") + private static final Class> SERVER_PLUGIN_CLASS = + (Class>) (Class) ServerPlugin.class; + + protected Configuration configuration; + + protected ModuleChecker moduleChecker; + + protected PluginManager> pluginManager; + + private final List> allPlugins; + + public ServerPluginManager(Configuration configuration, ModuleChecker moduleChecker) throws CircularDependencyException, InstantiationException, IllegalAccessException, ClassNotFoundException { + this.configuration = configuration; + this.moduleChecker = moduleChecker; + Builder> builder = new PluginManager.Builder<>(SERVER_PLUGIN_CLASS) + .withPluginsFromClasspath(); + this.allPlugins = new ArrayList<>(builder.getPlugins()); + this.pluginManager = builder.withPluginFilter(this::isPluginEnabled).build(); + } + + private ServerPluginManager(Configuration configuration, ModuleChecker moduleChecker, List> allPlugins) throws CircularDependencyException { + this.configuration = configuration; + this.moduleChecker = moduleChecker; + this.allPlugins = allPlugins; + this.pluginManager = new PluginManager.Builder<>(SERVER_PLUGIN_CLASS) + .withPlugins(allPlugins) + .withPluginFilter(this::isPluginEnabled) + .build(); + } + + public ServerPluginManager rebuild(ModuleChecker moduleChecker) throws CircularDependencyException { + return new ServerPluginManager(configuration, moduleChecker, allPlugins); + } + + // The proxy implements ServerPlugin at runtime; the cast to ServerPlugin + // is safe because all lifecycle calls go through the reflection-based PluginManager proxy. + @SuppressWarnings("unchecked") + public ServerPlugin getProxy() { + return (ServerPlugin) pluginManager.getProxy(); + } + + protected boolean isPluginEnabled(ServerPlugin plugin) { + String pluginName = plugin.getClass().getSimpleName(); + boolean enabled = configuration.getPropertyAsBoolean("plugins." + pluginName + ".enabled", true) + && (moduleChecker == null || moduleChecker.apply(plugin)); + if (!enabled && !plugin.canBeDisabled()) { + throw new PluginCriticalException("The plugin " + pluginName + " cannot be disabled"); + } + return enabled; + } + + public Configuration getConfiguration() { + return configuration; + } + + public PluginManager> getPluginManager() { + return pluginManager; + } + + public

> PluginManager

cloneAs(Class

pluginClass) throws CircularDependencyException { + PluginManager.Builder

builder = new PluginManager.Builder<>(pluginClass); + List

collect = this.getPluginManager().getPlugins().stream() + .filter(pluginClass::isInstance).map(pluginClass::cast).collect(Collectors.toList()); + return builder.withPlugins(collect).build(); + } +} \ No newline at end of file