package io.bootique;
import com.google.inject.Binder;
import com.google.inject.Binding;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.Module;
import com.google.inject.Provider;
import com.google.inject.Provides;
import com.google.inject.Singleton;
import com.google.inject.multibindings.MapBinder;
import com.google.inject.multibindings.Multibinder;
import io.bootique.annotation.Args;
import io.bootique.annotation.DefaultCommand;
import io.bootique.annotation.EnvironmentProperties;
import io.bootique.annotation.EnvironmentVariables;
import io.bootique.cli.Cli;
import io.bootique.command.Command;
import io.bootique.command.CommandManager;
import io.bootique.command.DefaultCommandManager;
import io.bootique.config.CliConfigurationSource;
import io.bootique.config.ConfigurationFactory;
import io.bootique.config.ConfigurationSource;
import io.bootique.config.PolymorphicConfiguration;
import io.bootique.config.TypesFactory;
import io.bootique.env.DeclaredVariable;
import io.bootique.env.DefaultEnvironment;
import io.bootique.env.Environment;
import io.bootique.help.DefaultHelpGenerator;
import io.bootique.help.HelpCommand;
import io.bootique.help.HelpGenerator;
import io.bootique.help.config.ConfigHelpGenerator;
import io.bootique.help.config.DefaultConfigHelpGenerator;
import io.bootique.help.config.HelpConfigCommand;
import io.bootique.jackson.DefaultJacksonService;
import io.bootique.jackson.JacksonService;
import io.bootique.jopt.JoptCliProvider;
import io.bootique.log.BootLogger;
import io.bootique.meta.application.ApplicationMetadata;
import io.bootique.meta.application.OptionMetadata;
import io.bootique.meta.config.ConfigHierarchyResolver;
import io.bootique.meta.config.ConfigMetadataCompiler;
import io.bootique.meta.module.ModulesMetadata;
import io.bootique.meta.module.ModulesMetadataCompiler;
import io.bootique.run.DefaultRunner;
import io.bootique.run.Runner;
import io.bootique.shutdown.ShutdownManager;
import io.bootique.shutdown.ShutdownTimeout;
import io.bootique.terminal.FixedWidthTerminal;
import io.bootique.terminal.SttyTerminal;
import io.bootique.terminal.Terminal;
import java.time.Duration;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
import java.util.logging.Level;
/**
* The main {@link Module} of Bootique DI runtime. Declares a minimal set of
* services needed for a Bootique app to start: services for parsing command
* line, reading configuration, selectign and running a Command.
*/
public class BQCoreModule implements Module {
// TODO: duplicate of FormattedAppender.MIN_LINE_WIDTH
private static final int TTY_MIN_COLUMNS = 40;
private static final int TTY_DEFAULT_COLUMNS = 80;
private String[] args;
private ShutdownManager shutdownManager;
private BootLogger bootLogger;
private Supplier<Collection<BQModule>> modulesSource;
private BQCoreModule() {
}
/**
* @return a Builder instance to configure the module before using it to
* initialize DI container.
* @since 0.12
*/
public static Builder builder() {
return new Builder();
}
/**
* Returns an instance of {@link BQCoreModuleExtender} used by downstream modules to load custom extensions to the
* Bootique core module. Should be invoked from a downstream Module's "configure" method.
*
* @param binder DI binder passed to the Module that invokes this method.
* @return an instance of {@link BQCoreModuleExtender} that can be used to load custom extensions to the Bootique
* core.
* @since 0.22
*/
public static BQCoreModuleExtender extend(Binder binder) {
return new BQCoreModuleExtender(binder);
}
/**
* @param binder DI binder passed to the Module that invokes this method.
* @return {@link Multibinder} for Bootique commands.
* @since 0.12
* @deprecated since 0.22 use {@link #extend(Binder)} to get an extender object, and
* then call {@link BQCoreModuleExtender#addCommand(Class)}.
*/
@Deprecated
public static Multibinder<Command> contributeCommands(Binder binder) {
return extend(binder).contributeCommands();
}
/**
* @param binder DI binder passed to the Module that invokes this method.
* @return {@link Multibinder} for Bootique options.
* @since 0.12
* @deprecated since 0.22 use {@link #extend(Binder)} to get an extender object, and
* then call {@link BQCoreModuleExtender#addOption(OptionMetadata)}.
*/
@Deprecated
public static Multibinder<OptionMetadata> contributeOptions(Binder binder) {
return extend(binder).contributeOptions();
}
/**
* @param binder DI binder passed to the Module that invokes this method.
* @return {@link MapBinder} for Bootique properties.
* @see EnvironmentProperties
* @since 0.12
* @deprecated since 0.22 use {@link #extend(Binder)} to get an extender object, and
* then call {@link BQCoreModuleExtender#setProperty(String, String)}.
*/
@Deprecated
public static MapBinder<String, String> contributeProperties(Binder binder) {
return extend(binder).contributeProperties();
}
/**
* @param binder DI binder passed to the Module that invokes this method.
* @return {@link MapBinder} for values emulating environment variables.
* @see EnvironmentVariables
* @since 0.17
* @deprecated since 0.22 use {@link #extend(Binder)} to get an extender object, and
* then call {@link BQCoreModuleExtender#setVar(String, String)}.
*/
@Deprecated
public static MapBinder<String, String> contributeVariables(Binder binder) {
return extend(binder).contributeVariables();
}
/**
* Provides a way to set default log levels for specific loggers. These settings can be overridden via Bootique
* configuration of whatever logging module you might use, like bootique-logback. This feature may be handy to
* suppress chatty third-party loggers, but still allow users to turn them on via configuration.
*
* @param binder DI binder passed to the Module that invokes this method.
* @return {@link MapBinder} for Bootique properties.
* @since 0.19
* @deprecated since 0.22 use {@link #extend(Binder)} to get an extender object, and
* then call {@link BQCoreModuleExtender#setLogLevel(String, Level)}.
*/
@Deprecated
public static MapBinder<String, Level> contributeLogLevels(Binder binder) {
return extend(binder).contributeLogLevels();
}
/**
* Binds an optional application description used in help messages, etc.
*
* @param description optional application description used in help messages, etc.
* @param binder DI binder passed to the Module that invokes this method.
* @since 0.20
* @deprecated since 0.22 use {@link #extend(Binder)} to get an extender object, and
* then call {@link BQCoreModuleExtender#setApplicationDescription(String)}.
*/
@Deprecated
public static void setApplicationDescription(Binder binder, String description) {
extend(binder).setApplicationDescription(description);
}
/**
* Initializes optional default command that will be executed if no explicit command is matched.
*
* @param binder DI binder passed to the Module that invokes this method.
* @param commandType a class of the default command.
* @since 0.20
* @deprecated since 0.22 use {@link #extend(Binder)} to get an extender object, and
* then call {@link BQCoreModuleExtender#setDefaultCommand(Class)}.
*/
@Deprecated
public static void setDefaultCommand(Binder binder, Class<? extends Command> commandType) {
extend(binder).setDefaultCommand(commandType);
}
/**
* Initializes optional default command that will be executed if no explicit command is matched.
*
* @param binder DI binder passed to the Module that invokes this method.
* @param command an instance of the default command.
* @since 0.20
* @deprecated since 0.22 use {@link #extend(Binder)} to get an extender object, and
* then call {@link BQCoreModuleExtender#setDefaultCommand(Command)}.
*/
@Deprecated
public static void setDefaultCommand(Binder binder, Command command) {
extend(binder).setDefaultCommand(command);
}
private static Optional<Command> defaultCommand(Injector injector) {
Binding<Command> binding = injector.getExistingBinding(Key.get(Command.class, DefaultCommand.class));
return binding != null ? Optional.of(binding.getProvider().get()) : Optional.empty();
}
@Override
public void configure(Binder binder) {
// trigger extension points creation and add default contributions
BQCoreModule.extend(binder)
.initAllExtensions()
.addOption(createConfigOption());
// bind instances
binder.bind(BootLogger.class).toInstance(Objects.requireNonNull(bootLogger));
binder.bind(ShutdownManager.class).toInstance(Objects.requireNonNull(shutdownManager));
binder.bind(String[].class).annotatedWith(Args.class).toInstance(Objects.requireNonNull(args));
// deprecated, kept for those users who may have injected this in their own code
binder.bind(Duration.class).annotatedWith(ShutdownTimeout.class)
.toInstance(Objects.requireNonNull(Duration.ofMillis(10000L)));
// too much code to create config factory.. extracting it in a provider
// class...
binder.bind(ConfigurationFactory.class).toProvider(JsonNodeConfigurationFactoryProvider.class).in(Singleton.class);
// we can't bind Provider with @Provides, so declaring it here...
binder.bind(Cli.class).toProvider(JoptCliProvider.class).in(Singleton.class);
// while "help" is a special command, we still store it in the common list of commands,
// so that "--help" is exposed as an explicit option
BQCoreModule.extend(binder).addCommand(HelpCommand.class);
BQCoreModule.extend(binder).addCommand(HelpConfigCommand.class);
}
OptionMetadata createConfigOption() {
return OptionMetadata
.builder(CliConfigurationSource.CONFIG_OPTION,
"Specifies YAML config location, which can be a file path or a URL.")
.valueRequired("yaml_location").build();
}
@Provides
@Singleton
JacksonService provideJacksonService(TypesFactory<PolymorphicConfiguration> typesFactory) {
return new DefaultJacksonService(typesFactory.getTypes());
}
@Provides
@Singleton
TypesFactory<PolymorphicConfiguration> provideConfigTypesFactory(BootLogger logger) {
return new TypesFactory<>(getClass().getClassLoader(), PolymorphicConfiguration.class, logger);
}
@Provides
@Singleton
Runner provideRunner(Cli cli, CommandManager commandManager) {
return new DefaultRunner(cli, commandManager);
}
@Provides
@Singleton
ConfigurationSource provideConfigurationSource(Cli cli, BootLogger bootLogger) {
return new CliConfigurationSource(cli, bootLogger);
}
@Provides
@Singleton
HelpCommand provideHelpCommand(BootLogger bootLogger, Provider<HelpGenerator> helpGeneratorProvider) {
return new HelpCommand(bootLogger, helpGeneratorProvider);
}
@Provides
@Singleton
HelpConfigCommand provideHelpConfigCommand(BootLogger bootLogger, Provider<ConfigHelpGenerator> helpGeneratorProvider) {
return new HelpConfigCommand(bootLogger, helpGeneratorProvider);
}
@Provides
@Singleton
CommandManager provideCommandManager(Set<Command> commands,
HelpCommand helpCommand,
Injector injector) {
// help command is bound, but default is optional, so check via injector...
Optional<Command> defaultCommand = defaultCommand(injector);
Map<String, Command> commandMap = new HashMap<>();
commands.forEach(c -> {
String name = c.getMetadata().getName();
// if command's name matches default command, exclude it from command map (it is implicit)
if (!defaultCommand.isPresent() || !defaultCommand.get().getMetadata().getName().equals(name)) {
Command existing = commandMap.put(name, c);
// complain on dupes
if (existing != null && existing != c) {
String c1 = existing.getClass().getName();
String c2 = c.getClass().getName();
String message = String.format("More than one DI command named '%s'. Conflicting types: %s, %s.",
name, c1, c2);
throw new BootiqueException(1, message);
}
}
});
return new DefaultCommandManager(commandMap, defaultCommand, Optional.of(helpCommand));
}
@Provides
@Singleton
HelpGenerator provideHelpGenerator(ApplicationMetadata application, Terminal terminal) {
int maxColumns = terminal.getColumns();
if (maxColumns < TTY_MIN_COLUMNS) {
maxColumns = TTY_DEFAULT_COLUMNS;
}
return new DefaultHelpGenerator(application, maxColumns);
}
@Provides
@Singleton
ConfigHelpGenerator provideConfigHelpGenerator(ModulesMetadata modulesMetadata, Terminal terminal) {
int maxColumns = terminal.getColumns();
if (maxColumns < TTY_MIN_COLUMNS) {
maxColumns = TTY_DEFAULT_COLUMNS;
}
return new DefaultConfigHelpGenerator(modulesMetadata, maxColumns);
}
@Provides
@Singleton
ConfigHierarchyResolver provideConfigHierarchyResolver(TypesFactory<PolymorphicConfiguration> typesFactory) {
return ConfigHierarchyResolver.create(typesFactory.getTypes());
}
@Provides
@Singleton
ModulesMetadata provideModulesMetadata(ConfigHierarchyResolver hierarchyResolver) {
return new ModulesMetadataCompiler(new ConfigMetadataCompiler(hierarchyResolver::directSubclasses))
.compile(this.modulesSource != null ? modulesSource.get() : Collections.emptyList());
}
@Provides
@Singleton
ApplicationMetadata provideApplicationMetadata(ApplicationDescription descriptionHolder,
CommandManager commandManager,
Set<OptionMetadata> options,
Set<DeclaredVariable> declaredVariables,
ModulesMetadata modulesMetadata) {
ApplicationMetadata.Builder builder = ApplicationMetadata
.builder()
.description(descriptionHolder.getDescription())
.addOptions(options);
commandManager.getCommands().values().forEach(c -> builder.addCommand(c.getMetadata()));
// merge default command options with top-level app options
commandManager.getDefaultCommand().ifPresent(c -> builder.addOptions(c.getMetadata().getOptions()));
new DeclaredVariableMetaResolver(modulesMetadata).resolve(declaredVariables).forEach(builder::addVariable);
return builder.build();
}
@Provides
@Singleton
Environment provideEnvironment(@EnvironmentProperties Map<String, String> diProperties,
@EnvironmentVariables Map<String, String> diVars,
Set<DeclaredVariable> declaredVariables) {
return DefaultEnvironment.withSystemPropertiesAndVariables()
.properties(diProperties)
.variables(diVars)
.declaredVariables(declaredVariables)
.build();
}
@Provides
@Singleton
Terminal provideTerminal(BootLogger bootLogger) {
// very simple OS test...
boolean isUnix = "/".equals(System.getProperty("file.separator"));
return isUnix ? new SttyTerminal(bootLogger) : new FixedWidthTerminal(TTY_DEFAULT_COLUMNS);
}
public static class Builder {
private BQCoreModule module;
private Builder() {
this.module = new BQCoreModule();
}
public BQCoreModule build() {
return module;
}
public Builder bootLogger(BootLogger bootLogger) {
module.bootLogger = bootLogger;
return this;
}
public Builder shutdownManager(ShutdownManager shutdownManager) {
module.shutdownManager = shutdownManager;
return this;
}
/**
* Sets a supplier of the app modules collection. It has to be provided externally by Bootique code that
* assembles the stack. We have no way of discovering this information when inside the DI container.
*
* @param modulesSource a supplier of module collection.
* @return this builder instance.
* @since 0.21
*/
public Builder moduleSource(Supplier<Collection<BQModule>> modulesSource) {
module.modulesSource = modulesSource;
return this;
}
public Builder args(String[] args) {
module.args = args;
return this;
}
}
}