package ru.vyarus.dropwizard.guice.module.context; import com.google.common.base.Preconditions; import com.google.common.collect.LinkedHashMultimap; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Multimap; import com.google.inject.Module; import io.dropwizard.Application; import io.dropwizard.Bundle; import io.dropwizard.cli.Command; import ru.vyarus.dropwizard.guice.bundle.GuiceyBundleLookup; import ru.vyarus.dropwizard.guice.module.context.info.ItemInfo; import ru.vyarus.dropwizard.guice.module.context.info.impl.ExtensionItemInfoImpl; import ru.vyarus.dropwizard.guice.module.context.info.impl.ItemInfoImpl; import ru.vyarus.dropwizard.guice.module.context.info.sign.DisableSupport; import ru.vyarus.dropwizard.guice.module.context.option.Option; import ru.vyarus.dropwizard.guice.module.context.option.internal.OptionsSupport; import ru.vyarus.dropwizard.guice.module.context.stat.StatsTracker; import ru.vyarus.dropwizard.guice.module.installer.FeatureInstaller; import ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBundle; import ru.vyarus.dropwizard.guice.module.installer.scanner.ClasspathScanner; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Map; /** * Configuration context used internally to track all registered configuration items. * Items may be registered by type (installer, extension) or by instance (module, bundle). * <p> * Each item is registered only once, but all registrations are tracked. Uniqueness guaranteed by type. * <p> * Support generic disabling mechanism (for items marked with {@link DisableSupport} sign). If item is disabled, but * never registered special empty item info will be created at the end of configuration. * <p> * Considered as internal api. * * @author Vyacheslav Rusakov * @see ItemInfo for details of tracked info * @see ConfigurationInfo for acessing collected info at runtime * @since 06.07.2016 */ @SuppressWarnings("PMD.GodClass") public final class ConfigurationContext { /** * Configured items (bundles, installers, extensions etc). * Preserve registration order. */ private final Multimap<ConfigItem, Object> itemsHolder = LinkedHashMultimap.create(); /** * Configuration details (stored mostly for diagnostics). */ private final Map<Class<?>, ItemInfo> detailsHolder = Maps.newHashMap(); /** * Holds disabled entries separately. Preserve registration order. */ private final Multimap<ConfigItem, Class<?>> disabledItemsHolder = LinkedHashMultimap.create(); /** * Holds disable source for disabled items. */ private final Multimap<Class<?>, Class<?>> disabledByHolder = LinkedHashMultimap.create(); /** * Current scope hierarchy. The last one is actual scope (application or bundle). */ private Class<?> currentScope; /** * Used to gather guicey startup metrics. */ private final StatsTracker tracker = new StatsTracker(); /** * Used to set and get options within guicey. */ private final OptionsSupport optionsSupport = new OptionsSupport(); // --------------------------------------------------------------------------- SCOPE /** * Current configuration context (application, bundle or classpath scan). * * @param scope scope class */ public void setScope(final Class<?> scope) { Preconditions.checkState(currentScope == null, "State error: current scope not closed"); currentScope = scope; } /** * Clears current scope. */ @SuppressWarnings("PMD.NullAssignment") public void closeScope() { Preconditions.checkState(currentScope != null, "State error: trying to close not opened scope"); currentScope = null; } // --------------------------------------------------------------------------- COMMANDS /** * Register commands resolved with classpath scan. * * @param commands installed commands * @see ru.vyarus.dropwizard.guice.GuiceBundle.Builder#searchCommands() */ public void registerCommands(final List<Class<Command>> commands) { setScope(ClasspathScanner.class); for (Class<Command> cmd : commands) { register(ConfigItem.Command, cmd); } closeScope(); } // --------------------------------------------------------------------------- BUNDLES /** * Register bundles, recognized from dropwizard bundles. {@link Bundle} used as context. * * @param bundles recognized bundles * @see ru.vyarus.dropwizard.guice.GuiceBundle.Builder#configureFromDropwizardBundles() */ public void registerDwBundles(final List<GuiceyBundle> bundles) { setScope(Bundle.class); for (GuiceyBundle bundle : bundles) { register(ConfigItem.Bundle, bundle); } closeScope(); } /** * Register bundles resolved by lookup mechanism. {@link GuiceyBundleLookup} used as context. * * @param bundles bundles resolved by lookup mechanism * @see GuiceyBundleLookup */ public void registerLookupBundles(final List<GuiceyBundle> bundles) { setScope(GuiceyBundleLookup.class); for (GuiceyBundle bundle : bundles) { register(ConfigItem.Bundle, bundle); } closeScope(); } /** * Usual bundle registration from {@link ru.vyarus.dropwizard.guice.GuiceBundle.Builder#bundles(GuiceyBundle...)} * or {@link ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap#bundles(GuiceyBundle...)}. * Context class is set to currently processed bundle. * * @param bundles bundles to register */ public void registerBundles(final GuiceyBundle... bundles) { for (GuiceyBundle bundle : bundles) { register(ConfigItem.Bundle, bundle); } } /** * @return all configured bundles (without duplicates) */ public List<GuiceyBundle> getBundles() { return getItems(ConfigItem.Bundle); } // --------------------------------------------------------------------------- MODULES /** * @param modules guice module to register */ public void registerModules(final Module... modules) { for (Module module : modules) { register(ConfigItem.Module, module); } } /** * @return all configured guice modules (without duplicates) */ public List<Module> getModules() { return getItems(ConfigItem.Module); } // --------------------------------------------------------------------------- INSTALLERS /** * Usual installer registration from {@link ru.vyarus.dropwizard.guice.GuiceBundle.Builder#installers(Class[])} * or {@link ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap#installers(Class[])}. * * @param installers installers to register */ @SuppressWarnings("PMD.UseVarargs") public void registerInstallers(final Class<? extends FeatureInstaller>[] installers) { for (Class<? extends FeatureInstaller> installer : installers) { register(ConfigItem.Installer, installer); } } /** * Register installers from classpath scan. Use {@link ClasspathScanner} as context class. * * @param installers installers found by classpath scan */ public void registerInstallersFromScan(final List<Class<? extends FeatureInstaller>> installers) { setScope(ClasspathScanner.class); for (Class<? extends FeatureInstaller> installer : installers) { register(ConfigItem.Installer, installer); } closeScope(); } /** * Installer manual disable registration from * {@link ru.vyarus.dropwizard.guice.GuiceBundle.Builder#disableInstallers(Class[])} * or {@link ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap#disableInstallers(Class[])}. * * @param installers installers to disable */ @SuppressWarnings("PMD.UseVarargs") public void disableInstallers(final Class<? extends FeatureInstaller>[] installers) { for (Class<? extends FeatureInstaller> installer : installers) { registerDisable(ConfigItem.Installer, installer); } } /** * Returns disabled installers too! * If called after {@link #finalizeConfiguration()} then may return never registered but disabled installers. * * @return all configured installers (including resolved by scan) */ public List<Class<? extends FeatureInstaller>> getInstallers() { return getItems(ConfigItem.Installer); } /** * @return all disabled installers (without duplicates) */ public List<Class<? extends FeatureInstaller>> getDisabledInstallers() { return getDisabledItems(ConfigItem.Installer); } // --------------------------------------------------------------------------- EXTENSIONS /** * Usual extension registration from {@link ru.vyarus.dropwizard.guice.GuiceBundle.Builder#extensions(Class[])} * or {@link ru.vyarus.dropwizard.guice.module.installer.bundle.GuiceyBootstrap#extensions(Class[])}. * * @param extensions extensions to register */ public void registerExtensions(final Class<?>... extensions) { for (Class<?> extension : extensions) { register(ConfigItem.Extension, extension); } } /** * Extensions classpath scan requires testing with all installers to recognize actual extensions. * To avoid duplicate installers recognition, extensions resolved by classpath scan are registered * immediately. It's required because of not obvious method used for both manually registered extensions * (to obtain container) and to create container from extensions from classpath scan. * * @param extension found extension * @param fromScan true when called for extension found in classpath scan, false for manually * registered extension * @return extension info container */ public ExtensionItemInfoImpl getOrRegisterExtension(final Class<?> extension, final boolean fromScan) { final ExtensionItemInfoImpl info; if (fromScan) { setScope(ClasspathScanner.class); info = register(ConfigItem.Extension, extension); closeScope(); } else { // info will be available for sure because such type was stored before (during manual registration) info = getInfo(extension); } return info; } /** * @return all configured extensions (including resolved by scan) */ public List<Class<?>> getExtensions() { return getItems(ConfigItem.Extension); } // --------------------------------------------------------------------------- OPTIONS /** * @param option option enum * @param value option value (not null) * @param <T> helper type to define option */ @SuppressWarnings("unchecked") public <T extends Enum & Option> void setOption(final T option, final Object value) { optionsSupport.set(option, value); } /** * @param option option enum * @param <V> value type * @param <T> helper type to define option * @return option value (set or default) */ @SuppressWarnings("unchecked") public <V, T extends Enum & Option> V option(final T option) { return (V) optionsSupport.get(option); } /** * @return options support object */ public OptionsSupport options() { return optionsSupport; } // --------------------------------------------------------------------------- GENERAL /** * Called when context configuration is finished (but extensions installation is not finished yet). * Merges disabled items configuration with registered items or creates new items to hold disable info. */ public void finalizeConfiguration() { for (ConfigItem type : disabledItemsHolder.keys()) { for (Object item : getDisabledItems(type)) { final DisableSupport info = getOrCreateInfo(type, item); info.getDisabledBy().addAll(disabledByHolder.get(getType(item))); } } } /** * @param type config type * @param <T> expected container type * @return list of all registered items of type or empty list */ @SuppressWarnings("unchecked") public <T> List<T> getItems(final ConfigItem type) { final Collection<Object> res = itemsHolder.get(type); return res.isEmpty() ? Collections.<T>emptyList() : (List<T>) Lists.newArrayList(res); } /** * Note: registration is always tracked by class, so when instance provided actual info will be returned * for object class. * * @param item item to get info * @param <T> expected container type * @return item registration info container or null if item not registered */ @SuppressWarnings("unchecked") public <T extends ItemInfoImpl> T getInfo(final Object item) { final Class<?> itemType = getType(item); return (T) detailsHolder.get(itemType); } /** * @return startup statistics tracker instance */ public StatsTracker stat() { return tracker; } private Class<?> getScope() { return currentScope == null ? Application.class : currentScope; } private void registerDisable(final ConfigItem type, final Class<?> item) { // multimaps will filter duplicates automatically disabledItemsHolder.put(type, item); disabledByHolder.put(item, getScope()); } private <T extends ItemInfoImpl> T register(final ConfigItem type, final Object item) { final T info = getOrCreateInfo(type, item); // if registered multiple times in one scope attempts will reveal it info.countRegistrationAttempt(); info.getRegisteredBy().add(getScope()); // first registration scope stored if (info.getRegistrationScope() == null) { info.setRegistrationScope(getScope()); } return info; } /** * Disabled items may not be actually registered. In order to register disable info in uniform way * dummy container is created. * * @param type item type * @param item item object * @param <T> expected container type * @return info container instance */ @SuppressWarnings("unchecked") private <T extends ItemInfoImpl> T getOrCreateInfo(final ConfigItem type, final Object item) { final Class<?> itemType = getType(item); final T info; // details holder allows to implicitly filter by type and avoid duplicate registration if (detailsHolder.containsKey(itemType)) { // no duplicate registration info = (T) detailsHolder.get(itemType); } else { itemsHolder.put(type, item); info = type.newContainer(itemType); detailsHolder.put(itemType, info); } return info; } private Class<?> getType(final Object item) { return item instanceof Class ? (Class) item : item.getClass(); } @SuppressWarnings("unchecked") private <T> List<T> getDisabledItems(final ConfigItem type) { final Collection<Class<?>> res = disabledItemsHolder.get(type); return res.isEmpty() ? Collections.<T>emptyList() : (List<T>) Lists.newArrayList(res); } }