Java tutorial
/* * This file is part of QuickStart Module Loader, licensed under the MIT License (MIT). See the LICENSE.txt file * at the root of this project for more details. */ package uk.co.drnaylor.quickstart; import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import ninja.leaping.configurate.ConfigurationNode; import ninja.leaping.configurate.ConfigurationOptions; import ninja.leaping.configurate.loader.ConfigurationLoader; import ninja.leaping.configurate.objectmapping.ObjectMappingException; import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable; import uk.co.drnaylor.quickstart.annotations.ModuleData; import uk.co.drnaylor.quickstart.config.AbstractConfigAdapter; import uk.co.drnaylor.quickstart.config.NoMergeIfPresent; import uk.co.drnaylor.quickstart.config.TypedAbstractConfigAdapter; import uk.co.drnaylor.quickstart.enums.ConstructionPhase; import uk.co.drnaylor.quickstart.enums.LoadingStatus; import uk.co.drnaylor.quickstart.enums.ModulePhase; import uk.co.drnaylor.quickstart.exceptions.IncorrectAdapterTypeException; import uk.co.drnaylor.quickstart.exceptions.MissingDependencyException; import uk.co.drnaylor.quickstart.exceptions.NoModuleException; import uk.co.drnaylor.quickstart.exceptions.QuickStartModuleDiscoveryException; import uk.co.drnaylor.quickstart.exceptions.QuickStartModuleLoaderException; import uk.co.drnaylor.quickstart.exceptions.UndisableableModuleException; import uk.co.drnaylor.quickstart.loaders.ModuleEnabler; import java.io.IOException; import java.text.MessageFormat; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import javax.annotation.Nullable; /** * The ModuleContainer contains all modules for a particular modular system. It scans the provided {@link ClassLoader} * classpath for {@link Module}s which has a root in the provided package. It handles all the discovery, module config * file generation, loading and enabling of modules. * * <p> * A system may have multiple module containers. Each module container is completely separate from one another. * </p> */ public abstract class ModuleContainer { /** * The current phase of the container. */ private ConstructionPhase currentPhase = ConstructionPhase.INITALISED; /** * The modules that have been discovered by the container. */ protected final Map<String, ModuleSpec> discoveredModules = Maps.newLinkedHashMap(); /** * Loaded modules that can be disabled. */ protected final Map<String, Module.RuntimeDisableable> enabledDisableableModules = Maps.newHashMap(); /** * Contains the main configuration file. */ protected final SystemConfig<?, ? extends ConfigurationLoader<?>> config; /** * The logger to use. */ protected final LoggerProxy loggerProxy; /** * Fires when the PREENABLE phase starts. */ private final Procedure onPreEnable; /** * Fires when the ENABLE phase starts. */ private final Procedure onEnable; /** * Fires when the POSTENABLE phase starts. */ private final Procedure onPostEnable; /** * Provides a way to enable modules. */ private final ModuleEnabler enabler; /** * Whether the {@link ModuleData} annotation must be present on modules. */ private final boolean requireAnnotation; /** * Whether or not to take note of {@link NoMergeIfPresent} annotations on configs. */ private final boolean processDoNotMerge; /** * The function that determines configuration headers for an entry. */ private final Function<Module, String> headerProcessor; /** * The function that determines the descriptions for a module's name. */ private final Function<Class<? extends Module>, String> descriptionProcessor; /** * The name of the configuration section that contains the module flags */ private final String moduleSection; /** * The header of the configuration section that contains the module flags */ @Nullable private final String moduleSectionHeader; /** * Constructs a {@link ModuleContainer} and starts discovery of the modules. * * @param <N> The type of {@link ConfigurationNode} to use. * @param configurationLoader The {@link ConfigurationLoader} that contains details of whether the modules should be enabled or not. * @param moduleEnabler The {@link ModuleEnabler} that contains the logic to enable modules. * @param loggerProxy The {@link LoggerProxy} that contains methods to send messages to the logger, or any other source. * @param onPreEnable The {@link Procedure} to run on pre enable, before modules are pre-enabled. * @param onEnable The {@link Procedure} to run on enable, before modules are pre-enabled. * @param onPostEnable The {@link Procedure} to run on post enable, before modules are pre-enabled. * @param configOptions The {@link Function} that converts {@link ConfigurationOptions}. * @param requireAnnotation Whether modules must have the {@link ModuleData} annotation. * @param processDoNotMerge Whether module configs will have {@link NoMergeIfPresent} annotations processed. * @param headerProcessor The {@link Function} to use when adding headers to module config sections. {@code null} means no headers. * @param descriptionProcessor The {@link Function} to use when adding descriptions to modules. {@code null} means no descriptions. * @param moduleSection The name of the section that contains the module enable/disable switches. * @param moduleSectionHeader The comment header for the "module" section * * @throws QuickStartModuleDiscoveryException if there is an error starting the Module Container. */ protected <N extends ConfigurationNode> ModuleContainer(ConfigurationLoader<N> configurationLoader, LoggerProxy loggerProxy, ModuleEnabler moduleEnabler, Procedure onPreEnable, Procedure onEnable, Procedure onPostEnable, Function<ConfigurationOptions, ConfigurationOptions> configOptions, boolean requireAnnotation, boolean processDoNotMerge, @Nullable Function<Module, String> headerProcessor, @Nullable Function<Class<? extends Module>, String> descriptionProcessor, String moduleSection, @Nullable String moduleSectionHeader) throws QuickStartModuleDiscoveryException { try { this.config = new SystemConfig<>(configurationLoader, loggerProxy, configOptions); this.loggerProxy = loggerProxy; this.enabler = moduleEnabler; this.onPreEnable = onPreEnable; this.onPostEnable = onPostEnable; this.onEnable = onEnable; this.requireAnnotation = requireAnnotation; this.processDoNotMerge = processDoNotMerge; this.descriptionProcessor = descriptionProcessor == null ? m -> { ModuleData md = m.getAnnotation(ModuleData.class); if (md != null) { return md.description(); } return ""; } : descriptionProcessor; this.headerProcessor = headerProcessor == null ? m -> "" : headerProcessor; this.moduleSection = moduleSection; this.moduleSectionHeader = moduleSectionHeader; } catch (Exception e) { throw new QuickStartModuleDiscoveryException("Unable to start QuickStart", e); } } public final void startDiscover() throws QuickStartModuleDiscoveryException { try { Preconditions.checkState(currentPhase == ConstructionPhase.INITALISED); currentPhase = ConstructionPhase.DISCOVERING; Set<Class<? extends Module>> modules = discoverModules(); HashMap<String, ModuleSpec> discovered = Maps.newHashMap(); for (Class<? extends Module> s : modules) { // If we have a module annotation, we are golden. String id; ModuleSpec ms; if (s.isAnnotationPresent(ModuleData.class)) { ModuleData md = s.getAnnotation(ModuleData.class); id = md.id().toLowerCase(); ms = new ModuleSpec(s, md); } else if (this.requireAnnotation) { loggerProxy.warn(MessageFormat.format( "The module class {0} does not have a ModuleData annotation associated with it. " + "It is not being loaded as the module container requires the annotation to be present.", s.getClass().getName())); continue; } else { id = s.getClass().getName().toLowerCase(); loggerProxy.warn(MessageFormat.format( "The module {0} does not have a ModuleData annotation associated with it. We're just assuming an ID of {0}.", id)); ms = new ModuleSpec(s, id, id, LoadingStatus.ENABLED, false); } if (discovered.containsKey(id)) { throw new QuickStartModuleDiscoveryException( "Duplicate module ID \"" + id + "\" was discovered - loading cannot continue."); } discovered.put(id, ms); } // Create the dependency map. resolveDependencyOrder(discovered); // Modules discovered. Create the Module Config adapter. List<ModuleSpec> moduleSpecList = this.discoveredModules.entrySet().stream() .filter(x -> !x.getValue().isMandatory()).map(Map.Entry::getValue).collect(Collectors.toList()); // Attaches config adapter and loads in the defaults. config.attachModulesConfig(moduleSpecList, this.descriptionProcessor, this.moduleSection, this.moduleSectionHeader); config.saveAdapterDefaults(false); // Load what we have in config into our discovered modules. try { config.getConfigAdapter().getNode().forEach((k, v) -> { try { ModuleSpec ms = discoveredModules.get(k); if (ms != null) { ms.setStatus(v); } else { loggerProxy.warn(String.format( "Ignoring module entry %s in the configuration file: module does not exist.", k)); } } catch (IllegalStateException ex) { loggerProxy.warn( "A mandatory module can't have its status changed by config. Falling back to FORCELOAD for " + k); } }); } catch (ObjectMappingException e) { loggerProxy.warn("Could not load modules config, falling back to defaults."); e.printStackTrace(); } // Modules have been discovered. currentPhase = ConstructionPhase.DISCOVERED; } catch (QuickStartModuleDiscoveryException ex) { throw ex; } catch (Exception e) { throw new QuickStartModuleDiscoveryException("Unable to discover QuickStart modules", e); } } private void resolveDependencyOrder(Map<String, ModuleSpec> modules) throws Exception { // First, get the modules that have no deps. processDependencyStep(modules, x -> x.getValue().getDependencies().isEmpty() && x.getValue().getSoftDependencies().isEmpty()); while (!modules.isEmpty()) { Set<String> addedModules = discoveredModules.keySet(); processDependencyStep(modules, x -> addedModules.containsAll(x.getValue().getDependencies()) && addedModules.containsAll(x.getValue().getSoftDependencies())); } } private void processDependencyStep(Map<String, ModuleSpec> modules, Predicate<Map.Entry<String, ModuleSpec>> predicate) throws Exception { // Filter on the predicate List<Map.Entry<String, ModuleSpec>> modulesToAdd = modules.entrySet().stream().filter(predicate) .sorted((x, y) -> x.getValue().isMandatory() == y.getValue().isMandatory() ? x.getKey().compareTo(y.getKey()) : Boolean.compare(x.getValue().isMandatory(), y.getValue().isMandatory())) .collect(Collectors.toList()); if (modulesToAdd.isEmpty()) { throw new IllegalStateException("Some modules have circular dependencies: " + modules.keySet().stream().collect(Collectors.joining(", "))); } modulesToAdd.forEach(x -> { discoveredModules.put(x.getKey(), x.getValue()); modules.remove(x.getKey()); }); } private boolean dependenciesSatisfied(ModuleSpec moduleSpec, Set<String> enabledModules) { if (moduleSpec.getDependencies().isEmpty()) { return true; } for (String m : moduleSpec.getDependencies()) { if (!enabledModules.contains(m) || !dependenciesSatisfied(this.discoveredModules.get(m), enabledModules)) { return false; } } // We know the deps are satisfied. return true; } protected abstract Set<Class<? extends Module>> discoverModules() throws Exception; /** * Gets the current phase of the module loader. * * @return The {@link ConstructionPhase} */ public ConstructionPhase getCurrentPhase() { return currentPhase; } /** * Gets a set of IDs of modules that are going to be loaded. * * @return The modules that are going to be loaded. */ public Set<String> getModules() { return getModules(ModuleStatusTristate.ENABLE); } /** * Gets a set of IDs of modules. * * @param enabledOnly If <code>true</code>, only return modules that are going to be loaded. * @return The modules. */ public Set<String> getModules(final ModuleStatusTristate enabledOnly) { Preconditions.checkNotNull(enabledOnly); Preconditions.checkState( currentPhase != ConstructionPhase.INITALISED && currentPhase != ConstructionPhase.DISCOVERING); return discoveredModules.entrySet().stream().filter(enabledOnly.statusPredicate).map(Map.Entry::getKey) .collect(Collectors.toSet()); } /** * Gets an immutable {@link Map} of module IDs to their {@link LoadingStatus} (disabled, enabled, forceload). * * @return The modules with their loading states. */ public Map<String, LoadingStatus> getModulesWithLoadingState() { Preconditions.checkState( currentPhase != ConstructionPhase.INITALISED && currentPhase != ConstructionPhase.DISCOVERING); return ImmutableMap.copyOf(discoveredModules.entrySet().stream() .collect(Collectors.toMap(Map.Entry::getKey, v -> v.getValue().getStatus()))); } /** * Gets whether a module is enabled and loaded. * * @param moduleId The module ID to check for. * @return <code>true</code> if it is enabled. * @throws NoModuleException Thrown if the module does not exist and modules have been loaded. */ public boolean isModuleLoaded(String moduleId) throws NoModuleException { if (currentPhase != ConstructionPhase.ENABLING && currentPhase != ConstructionPhase.ENABLED) { return false; } ModuleSpec ms = discoveredModules.get(moduleId.toLowerCase()); if (ms == null) { // No module throw new NoModuleException(moduleId); } return ms.getPhase() == ModulePhase.ENABLED; } /** * Requests that a module be disabled. This can only be run during the {@link ConstructionPhase#DISCOVERED} phase, or for * {@link Module.RuntimeDisableable} modules, {@link ConstructionPhase#ENABLED}. * * @param moduleName The ID of the module. * @throws UndisableableModuleException if the module can't be disabled. * @throws NoModuleException if the module does not exist. */ public void disableModule(String moduleName) throws UndisableableModuleException, NoModuleException { if (currentPhase == ConstructionPhase.DISCOVERED) { ModuleSpec ms = discoveredModules.get(moduleName.toLowerCase()); if (ms == null) { // No module throw new NoModuleException(moduleName); } if (ms.isMandatory() || ms.getStatus() == LoadingStatus.FORCELOAD) { throw new UndisableableModuleException(moduleName); } ms.setStatus(LoadingStatus.DISABLED); } else { Preconditions.checkState(currentPhase == ConstructionPhase.ENABLED); ModuleSpec ms = discoveredModules.get(moduleName.toLowerCase()); Module.RuntimeDisableable m = enabledDisableableModules.get(moduleName.toLowerCase()); if (!ms.isRuntimeAlterable()) { throw new UndisableableModuleException(moduleName.toLowerCase(), "Cannot disable this module at runtime!"); } Preconditions.checkState(ms.getPhase() != ModulePhase.ERRORED, "Cannot disable this module as it errored!"); Preconditions.checkState(ms.getPhase() == ModulePhase.ENABLED, "Cannot disable this module as it is not enabled!"); m.onDisable(); detachConfig(ms.getName()); ms.setPhase(ModulePhase.DISABLED); enabledDisableableModules.remove(moduleName.toLowerCase()); } } protected abstract Module getModule(ModuleSpec spec) throws Exception; /** * Starts the module construction and enabling phase. This is the final phase for loading the modules. * * <p> * Once this method is called, modules can no longer be removed. * </p> * * @param failOnOneError If set to <code>true</code>, one module failure will mark the whole loading sequence as failed. * Otherwise, no modules being constructed will cause a failure. * * @throws QuickStartModuleLoaderException.Construction if the modules cannot be constructed. * @throws QuickStartModuleLoaderException.Enabling if the modules cannot be enabled. */ public void loadModules(boolean failOnOneError) throws QuickStartModuleLoaderException.Construction, QuickStartModuleLoaderException.Enabling { Preconditions.checkArgument(currentPhase == ConstructionPhase.DISCOVERED); currentPhase = ConstructionPhase.ENABLING; // Get the modules that are being disabled and mark them as such. Set<String> disabledModules = getModules(ModuleStatusTristate.DISABLE); while (!disabledModules.isEmpty()) { // Find any modules that have dependencies on disabled modules, and disable them. List<ModuleSpec> toDisable = getModules(ModuleStatusTristate.ENABLE).stream() .map(discoveredModules::get) .filter(x -> !Collections.disjoint(disabledModules, x.getDependencies())) .collect(Collectors.toList()); if (toDisable.isEmpty()) { break; } if (toDisable.stream().anyMatch(ModuleSpec::isMandatory)) { String s = toDisable.stream().filter(ModuleSpec::isMandatory).map(ModuleSpec::getId) .collect(Collectors.joining(", ")); Class<? extends Module> m = toDisable.stream().filter(ModuleSpec::isMandatory).findFirst().get() .getModuleClass(); throw new QuickStartModuleLoaderException.Construction(m, "Tried to disable mandatory module", new IllegalStateException( "Dependency failure, tried to disable a mandatory module (" + s + ")")); } toDisable.forEach(k -> { k.setStatus(LoadingStatus.DISABLED); disabledModules.add(k.getId()); }); } // Make sure we get a clean slate here. getModules(ModuleStatusTristate.DISABLE) .forEach(k -> discoveredModules.get(k).setPhase(ModulePhase.DISABLED)); // Modules to enable. Map<String, Module> modules = Maps.newConcurrentMap(); // Construct them for (String s : getModules(ModuleStatusTristate.ENABLE)) { ModuleSpec ms = discoveredModules.get(s); try { modules.put(s, getModule(ms)); ms.setPhase(ModulePhase.CONSTRUCTED); } catch (Exception construction) { construction.printStackTrace(); ms.setPhase(ModulePhase.ERRORED); loggerProxy.error("The module " + ms.getModuleClass().getName() + " failed to construct."); if (failOnOneError) { currentPhase = ConstructionPhase.ERRORED; throw new QuickStartModuleLoaderException.Construction(ms.getModuleClass(), "The module " + ms.getModuleClass().getName() + " failed to construct.", construction); } } } if (modules.isEmpty()) { currentPhase = ConstructionPhase.ERRORED; throw new QuickStartModuleLoaderException.Construction(null, "No modules were constructed.", null); } int size = modules.size(); { Iterator<Map.Entry<String, Module>> im = modules.entrySet().iterator(); while (im.hasNext()) { Map.Entry<String, Module> module = im.next(); try { module.getValue().checkExternalDependencies(); } catch (MissingDependencyException ex) { this.discoveredModules.get(module.getKey()).setStatus(LoadingStatus.DISABLED); this.discoveredModules.get(module.getKey()).setPhase(ModulePhase.DISABLED); this.loggerProxy.warn("Module " + module.getKey() + " can not be enabled because an external dependency could not be satisfied."); this.loggerProxy.warn("Message was: " + ex.getMessage()); im.remove(); } } } while (size != modules.size()) { // We might need to disable modules. size = modules.size(); Iterator<Map.Entry<String, Module>> im = modules.entrySet().iterator(); while (im.hasNext()) { Map.Entry<String, Module> module = im.next(); if (!dependenciesSatisfied(this.discoveredModules.get(module.getKey()), getModules(ModuleStatusTristate.ENABLE))) { im.remove(); this.loggerProxy.warn("Module " + module.getKey() + " can not be enabled because an external dependency on a module it " + "depends on could not be satisfied."); this.discoveredModules.get(module.getKey()).setStatus(LoadingStatus.DISABLED); this.discoveredModules.get(module.getKey()).setPhase(ModulePhase.DISABLED); } } } // Enter Config Adapter phase - attaching before enabling so that enable methods can get any associated configurations. for (String s : modules.keySet()) { Module m = modules.get(s); try { attachConfig(s, m); } catch (Exception e) { e.printStackTrace(); if (failOnOneError) { throw new QuickStartModuleLoaderException.Enabling(m.getClass(), "Failed to attach config.", e); } } } // Enter Enable phase. Map<String, Module> c = new HashMap<>(modules); for (EnablePhase v : EnablePhase.values()) { loggerProxy.info(String.format("Starting phase: %s", v.name())); v.onStart(this); for (String s : c.keySet()) { ModuleSpec ms = discoveredModules.get(s); // If the module is errored, then we do not continue. if (ms.getPhase() == ModulePhase.ERRORED) { continue; } try { Module m = modules.get(s); v.onModuleAction(this, enabler, m, ms); } catch (Exception construction) { construction.printStackTrace(); modules.remove(s); if (v != EnablePhase.POSTENABLE) { ms.setPhase(ModulePhase.ERRORED); loggerProxy.error("The module " + ms.getModuleClass().getName() + " failed to enable."); if (failOnOneError) { currentPhase = ConstructionPhase.ERRORED; throw new QuickStartModuleLoaderException.Enabling(ms.getModuleClass(), "The module " + ms.getModuleClass().getName() + " failed to enable.", construction); } } else { loggerProxy .error("The module " + ms.getModuleClass().getName() + " failed to post-enable."); } } } } if (c.isEmpty()) { currentPhase = ConstructionPhase.ERRORED; throw new QuickStartModuleLoaderException.Enabling(null, "No modules were enabled.", null); } try { config.saveAdapterDefaults(this.processDoNotMerge); } catch (IOException e) { e.printStackTrace(); } currentPhase = ConstructionPhase.ENABLED; } /** * Enables a {@link Module.RuntimeDisableable} after the construction has completed. * * @param name The name of the module to load. * @throws Exception thrown if the module is not loadable for any reason, including if it is already enabled. */ public void runtimeEnable(String name) throws Exception { Preconditions.checkState(this.currentPhase == ConstructionPhase.ENABLED); Preconditions.checkState(!isModuleLoaded(name), "Module is already loaded!"); ModuleSpec ms = discoveredModules.get(name); Preconditions.checkState(Module.RuntimeDisableable.class.isAssignableFrom(ms.getModuleClass()), "Module " + name + " cannot be enabled at runtime!"); try { // Construction Module.RuntimeDisableable module = (Module.RuntimeDisableable) getModule(ms); ms.setPhase(ModulePhase.CONSTRUCTED); module.checkExternalDependencies(); // Enabling for (EnablePhase v : EnablePhase.values()) { try { v.onModuleAction(this, enabler, module, ms); } catch (Exception e) { if (v == EnablePhase.POSTENABLE) { loggerProxy .error("The module " + ms.getModuleClass().getName() + " failed to post-enable."); } else { throw e; } } } } catch (Exception construction) { ms.setPhase(ModulePhase.ERRORED); throw construction; } } private void attachConfig(String name, Module m) throws Exception { Optional<AbstractConfigAdapter<?>> a = m.getConfigAdapter(); if (a.isPresent()) { config.attachConfigAdapter(name, a.get(), this.headerProcessor.apply(m)); } } private void detachConfig(String name) { config.detachConfigAdapter(name); } @SuppressWarnings("unchecked") public final <R extends AbstractConfigAdapter<?>> R getConfigAdapterForModule(String module, Class<R> adapterClass) throws NoModuleException, IncorrectAdapterTypeException { return config.getConfigAdapterForModule(module, adapterClass); } /** * Saves the {@link SystemConfig}. * * @throws IOException If the config could not be saved. */ public final void saveSystemConfig() throws IOException { config.save(); } /** * Refreshes the backing {@link ConfigurationNode} and saves the {@link SystemConfig}. * * @throws IOException If the config could not be saved. */ public final void refreshSystemConfig() throws IOException { config.save(true); } /** * Reloads the {@link SystemConfig}, but does not change any module status. * * @throws IOException If the config could not be reloaded. */ public final void reloadSystemConfig() throws IOException { config.load(); } /** * Gets the registered module ID, if it exists. * * @param module The module. * @return The module ID, or an empty {@link Optional#empty()} */ public final Optional<String> getIdForModule(Module module) { return discoveredModules.entrySet().stream().filter(x -> x.getValue().getModuleClass() == module.getClass()) .map(Map.Entry::getKey).findFirst(); } /** * Builder class to create a {@link ModuleContainer} */ public static abstract class Builder<R extends ModuleContainer, T extends Builder<R, T>> { protected ConfigurationLoader<? extends ConfigurationNode> configurationLoader; protected boolean requireAnnotation = false; protected LoggerProxy loggerProxy; protected Procedure onPreEnable = () -> { }; protected Procedure onEnable = () -> { }; protected Procedure onPostEnable = () -> { }; protected Function<ConfigurationOptions, ConfigurationOptions> configurationOptionsTransformer = x -> x; protected ModuleEnabler enabler = ModuleEnabler.SIMPLE_INSTANCE; protected boolean doNotMerge = false; @Nullable protected Function<Class<? extends Module>, String> moduleDescriptionHandler = null; @Nullable protected Function<Module, String> moduleConfigurationHeader = null; protected String moduleConfigSection = "modules"; @Nullable protected String moduleDescription = null; protected abstract T getThis(); /** * Sets the {@link ConfigurationLoader} that will handle the module loading. * * @param configurationLoader The loader to use. * @return This {@link Builder}, for chaining. */ public T setConfigurationLoader(ConfigurationLoader<? extends ConfigurationNode> configurationLoader) { this.configurationLoader = configurationLoader; return getThis(); } /** * Sets a {@link Function} that takes the loader's {@link ConfigurationOptions}, transforms it, and applies it * to nodes when they are loaded. * * <p> * By default, just uses the {@link ConfigurationOptions} of the loader. * </p> * * @param optionsTransformer The transformer * @return This {@link Builder} for chaining. */ public T setConfigurationOptionsTransformer( Function<ConfigurationOptions, ConfigurationOptions> optionsTransformer) { Preconditions.checkNotNull(optionsTransformer); this.configurationOptionsTransformer = optionsTransformer; return getThis(); } /** * Sets the {@link LoggerProxy} to use for log messages. * * @param loggerProxy The logger proxy to use. * @return This {@link Builder}, for chaining. */ public T setLoggerProxy(LoggerProxy loggerProxy) { this.loggerProxy = loggerProxy; return getThis(); } /** * Sets the {@link Procedure} to run when the pre-enable phase is about to start. * * @param onPreEnable The {@link Procedure} * @return This {@link Builder}, for chaining. */ public T setOnPreEnable(Procedure onPreEnable) { Preconditions.checkNotNull(onPreEnable); this.onPreEnable = onPreEnable; return getThis(); } /** * Sets the {@link Procedure} to run when the enable phase is about to start. * * @param onEnable The {@link Procedure} * @return This {@link Builder}, for chaining. */ public T setOnEnable(Procedure onEnable) { Preconditions.checkNotNull(onEnable); this.onEnable = onEnable; return getThis(); } /** * Sets the {@link Procedure} to run when the post-enable phase is about to start. * * @param onPostEnable The {@link Procedure} * @return This {@link Builder}, for chaining. */ public T setOnPostEnable(Procedure onPostEnable) { Preconditions.checkNotNull(onPostEnable); this.onPostEnable = onPostEnable; return getThis(); } /** * Sets the {@link ModuleEnabler} to run when enabling modules. * * @param enabler The {@link ModuleEnabler}, or {@code null} when the default should be used. * @return This {@link Builder}, for chaining. */ public T setModuleEnabler(ModuleEnabler enabler) { this.enabler = enabler; return getThis(); } /** * Sets whether {@link Module}s must have a {@link ModuleData} annotation to be considered. * * @param requireAnnotation <code>true</code> to require, <code>false</code> otherwise. * @return The {@link Builder}, for chaining. */ public T setRequireModuleDataAnnotation(boolean requireAnnotation) { this.requireAnnotation = requireAnnotation; return getThis(); } /** * Sets whether {@link TypedAbstractConfigAdapter} {@link ConfigSerializable} fields that have the annotation {@link NoMergeIfPresent} * will <em>not</em> be merged into existing config values. * * @param noMergeIfPresent <code>true</code> if fields should be skipped if they are already populated. * @return This {@link Builder}, for chaining. */ public T setNoMergeIfPresent(boolean noMergeIfPresent) { this.doNotMerge = noMergeIfPresent; return getThis(); } /** * Sets the function that is used to set the description for each module in the configuration file. * * <p> * This is displayed above each of the module toggles in the configuration file. * </p> * * @param handler The {@link Function} to use, or {@code null} otherwise. * @return This {@link Builder}, for chaining. */ public T setModuleDescriptionHandler(@Nullable Function<Class<? extends Module>, String> handler) { this.moduleDescriptionHandler = handler; return getThis(); } /** * Sets the function that is used to set the header for each module's configuration block in the configuration file. * * <p> * This is displayed above each of the configuration sections in the configuration file. * </p> * * @param header The {@link Function} to use, or {@code null} otherwise. * @return This {@link Builder}, for chaining. */ public T setModuleConfigurationHeader(@Nullable Function<Module, String> header) { this.moduleConfigurationHeader = header; return getThis(); } /** * Sets the name of the section that contains the module enable/disable flags. * * @param name The name of the section. Defaults to "modules" * @return This {@link Builder}, for chaining. */ public T setModuleConfigSectionName(String name) { Preconditions.checkNotNull(name); this.moduleConfigSection = name; return getThis(); } /** * Sets the description for the module config section. * * @param description The description, or {@code null} to use the default. * @return This {@link Builder}, for chaining. */ public T setModuleConfigSectionDescription(@Nullable String description) { this.moduleDescription = description; return getThis(); } protected void checkBuild() { Preconditions.checkNotNull(configurationLoader); Preconditions.checkNotNull(moduleConfigSection); if (loggerProxy == null) { loggerProxy = DefaultLogger.INSTANCE; } if (enabler == null) { enabler = ModuleEnabler.SIMPLE_INSTANCE; } Metadata.getStartupMessage().ifPresent(x -> loggerProxy.info(x)); } public abstract R build() throws Exception; /** * Builds the module container and immediately starts discovery. * * @param startDiscover <code>true</code> if so. * @return The built module container. * @throws Exception if there was a problem during building or discovery. */ public final R build(boolean startDiscover) throws Exception { R build = build(); if (startDiscover) { build.startDiscover(); } return build; } } public enum ModuleStatusTristate { ENABLE(k -> k.getValue().getStatus() != LoadingStatus.DISABLED && k.getValue().getPhase() != ModulePhase.ERRORED && k.getValue().getPhase() != ModulePhase.DISABLED), DISABLE( k -> !ENABLE.statusPredicate.test(k)), ALL(k -> true); private final Predicate<Map.Entry<String, ModuleSpec>> statusPredicate; ModuleStatusTristate(Predicate<Map.Entry<String, ModuleSpec>> p) { statusPredicate = p; } } private interface ConstructPhase { void onStart(ModuleContainer container); void onModuleAction(ModuleContainer moduleContainer, ModuleEnabler enabler, Module module, ModuleSpec ms) throws Exception; } private enum EnablePhase implements ConstructPhase { PREENABLE { @Override public void onStart(ModuleContainer container) { container.onPreEnable.invoke(); } @Override public void onModuleAction(ModuleContainer moduleContainer, ModuleEnabler enabler, Module module, ModuleSpec ms) throws Exception { enabler.preEnableModule(module); } }, ENABLE { @Override public void onStart(ModuleContainer container) { container.onEnable.invoke(); } @Override public void onModuleAction(ModuleContainer moduleContainer, ModuleEnabler enabler, Module module, ModuleSpec ms) throws Exception { enabler.enableModule(module); ms.setPhase(ModulePhase.ENABLED); if (module instanceof Module.RuntimeDisableable) { moduleContainer.enabledDisableableModules.put(ms.getId(), (Module.RuntimeDisableable) module); } } }, POSTENABLE { @Override public void onStart(ModuleContainer container) { container.onPostEnable.invoke(); } @Override public void onModuleAction(ModuleContainer moduleContainer, ModuleEnabler enabler, Module module, ModuleSpec ms) throws Exception { enabler.postEnableModule(module); } } } }