Skip to content

Commit

Permalink
DI config binding API to facilitate configuration reuse #198
Browse files Browse the repository at this point in the history
* optional DI configs
  • Loading branch information
andrus committed Nov 28, 2017
1 parent b3d47bb commit a924172
Show file tree
Hide file tree
Showing 8 changed files with 309 additions and 168 deletions.
1 change: 1 addition & 0 deletions RELEASE-NOTES.md
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* #194 Refactor JoptCliProvider to CliFactory and generic Cli provider * #194 Refactor JoptCliProvider to CliFactory and generic Cli provider
* #195 CommandManager: allow lookup of commands by name and by type * #195 CommandManager: allow lookup of commands by name and by type
* #196 CommandManager: track command attributes and private commands * #196 CommandManager: track command attributes and private commands
* #198 DI config binding API to facilitate configuration reuse


## 0.24 ## 0.24


Expand Down
48 changes: 38 additions & 10 deletions bootique/src/main/java/io/bootique/BQCoreModuleExtender.java
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import io.bootique.command.Command; import io.bootique.command.Command;
import io.bootique.command.CommandDecorator; import io.bootique.command.CommandDecorator;
import io.bootique.command.CommandRefDecorated; import io.bootique.command.CommandRefDecorated;
import io.bootique.config.OptionRefWithConfig;
import io.bootique.env.DeclaredVariable; import io.bootique.env.DeclaredVariable;
import io.bootique.meta.application.OptionMetadata; import io.bootique.meta.application.OptionMetadata;


Expand All @@ -37,6 +38,8 @@ public class BQCoreModuleExtender extends ModuleExtender<BQCoreModuleExtender> {
private Multibinder<OptionMetadata> options; private Multibinder<OptionMetadata> options;
private Multibinder<Command> commands; private Multibinder<Command> commands;
private Multibinder<CommandRefDecorated> commandDecorators; private Multibinder<CommandRefDecorated> commandDecorators;
private Multibinder<OptionRefWithConfig> optionDecorators;



protected BQCoreModuleExtender(Binder binder) { protected BQCoreModuleExtender(Binder binder) {
super(binder); super(binder);
Expand All @@ -52,6 +55,7 @@ public BQCoreModuleExtender initAllExtensions() {
contributeOptions(); contributeOptions();
contributeCommands(); contributeCommands();
contributeCommandDecorators(); contributeCommandDecorators();
contributeOptionDecorators();


return this; return this;
} }
Expand Down Expand Up @@ -152,6 +156,23 @@ public BQCoreModuleExtender addConfig(String configResourceId) {
return this; return this;
} }


/**
* Decorates a CLI option with a URL of a configuration resource to be conditionally loaded by the app when the
* that option is selected. The config is loaded prior to any configuration potentially loaded via the option.
* This method can be called multiple times for the same option, adding multiple config decorators.
*
* @param configResourceId a resource path compatible with {@link io.bootique.resource.ResourceFactory} denoting
* a configuration source. E.g. "a/b/my.yml", or "classpath:com/foo/another.yml".
* @return this extender instance.
* @since 0.25
*/
public BQCoreModuleExtender addConfigOnOption(String decoratedOptionName, String configResourceId) {
// using Multibinder to support multiple decorators for the same option
contributeOptionDecorators().addBinding()
.toInstance(new OptionRefWithConfig(decoratedOptionName, configResourceId));
return this;
}

/** /**
* Adds a new option to the list of Bootique CLI options. * Adds a new option to the list of Bootique CLI options.
* *
Expand All @@ -177,12 +198,12 @@ public BQCoreModuleExtender addOptions(OptionMetadata... options) {
} }


/** /**
* Associates the CLI option with a config path. The option runtime value is assigned to the configuration property * Declares a new CLI option, associating it with a config path. The option runtime value is assigned to the
* denoted by the path. * configuration property denoted by the path.
* *
* @param configPath a dot-separated "path" that navigates configuration tree to the desired property. E.g. * @param configPath a dot-separated "path" that navigates configuration tree to the desired property. E.g.
* "jdbc.myds.password". * "jdbc.myds.password".
* @param name alias of an option * @param name the name of the new CLI option.
* @return this extender instance * @return this extender instance
* @since 0.24 * @since 0.24
*/ */
Expand All @@ -196,14 +217,14 @@ public BQCoreModuleExtender addOption(String configPath, String name) {
} }


/** /**
* Associates the CLI option with a config path. The option runtime value is assigned to the configuration property * Declares a new CLI option, associating it with a config path. The option runtime value is assigned to the
* denoted by the path. Default value provided here will be used if the option is present, but no value is specified * configuration property denoted by the path. Default value provided here will be used if the option is present,
* on the command line. * but no value is specified on the command line.
* *
* @param configPath a dot-separated "path" that navigates configuration tree to the desired property. E.g. * @param configPath a dot-separated "path" that navigates configuration tree to the desired property. E.g.
* "jdbc.myds.password". * "jdbc.myds.password".
* @param defaultValue default option value * @param defaultValue default option value
* @param name alias of an option * @param name the name of the new CLI option.
* @return this extender instance * @return this extender instance
* @since 0.24 * @since 0.24
*/ */
Expand All @@ -218,15 +239,18 @@ public BQCoreModuleExtender addOption(String configPath, String defaultValue, St
} }


/** /**
* Associates the CLI option value with a config resource. This way a single option can be used to enable a complex * Declares a new CLI option, associating its presence with a configuration resource. This way a single option
* configuration. * can be used to enable a complex configuration.
* *
* @param configResourceId a resource path compatible with {@link io.bootique.resource.ResourceFactory} denoting * @param configResourceId a resource path compatible with {@link io.bootique.resource.ResourceFactory} denoting
* a configuration source. E.g. "a/b/my.yml", or "classpath:com/foo/another.yml". * a configuration source. E.g. "a/b/my.yml", or "classpath:com/foo/another.yml".
* @param name alias of an option * @param name the name of the new CLI option.
* @return this extender instance * @return this extender instance
* @since 0.24 * @since 0.24
* @deprecated since 0.25. The new way of adding an option associated with a config file is by separately declaring
* an option and then associating it with one or more configs via {@link #addConfigOnOption(String, String)}.
*/ */
@Deprecated
public BQCoreModuleExtender addConfigResourceOption(String configResourceId, String name) { public BQCoreModuleExtender addConfigResourceOption(String configResourceId, String name) {
contributeOptions().addBinding().toInstance( contributeOptions().addBinding().toInstance(
OptionMetadata.builder(name) OptionMetadata.builder(name)
Expand Down Expand Up @@ -284,6 +308,10 @@ protected Multibinder<CommandRefDecorated> contributeCommandDecorators() {
return commandDecorators != null ? commandDecorators : (commandDecorators = newSet(CommandRefDecorated.class)); return commandDecorators != null ? commandDecorators : (commandDecorators = newSet(CommandRefDecorated.class));
} }


protected Multibinder<OptionRefWithConfig> contributeOptionDecorators() {
return optionDecorators != null ? optionDecorators : (optionDecorators = newSet(OptionRefWithConfig.class));
}

protected MapBinder<String, String> contributeProperties() { protected MapBinder<String, String> contributeProperties() {
return properties != null ? properties : (properties = newMap(String.class, String.class, EnvironmentProperties.class)); return properties != null ? properties : (properties = newMap(String.class, String.class, EnvironmentProperties.class));
} }
Expand Down
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import io.bootique.cli.Cli; import io.bootique.cli.Cli;
import io.bootique.config.ConfigurationFactory; import io.bootique.config.ConfigurationFactory;
import io.bootique.config.ConfigurationSource; import io.bootique.config.ConfigurationSource;
import io.bootique.config.OptionRefWithConfig;
import io.bootique.config.jackson.InPlaceResourceOverrider; import io.bootique.config.jackson.InPlaceResourceOverrider;
import io.bootique.config.jackson.InPlaceLeftHandMerger; import io.bootique.config.jackson.InPlaceLeftHandMerger;
import io.bootique.config.jackson.InPlaceMapOverrider; import io.bootique.config.jackson.InPlaceMapOverrider;
Expand All @@ -26,6 +27,7 @@
import java.io.InputStream; import java.io.InputStream;
import java.net.URL; import java.net.URL;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
Expand All @@ -44,7 +46,8 @@ public class JsonNodeConfigurationFactoryProvider implements Provider<Configurat
private Environment environment; private Environment environment;
private JacksonService jacksonService; private JacksonService jacksonService;
private BootLogger bootLogger; private BootLogger bootLogger;
private Set<OptionMetadata> optionMetadataSet; private Set<OptionMetadata> optionMetadata;
private Set<OptionRefWithConfig> optionDecorators;
private Cli cli; private Cli cli;


@Inject @Inject
Expand All @@ -53,14 +56,16 @@ public JsonNodeConfigurationFactoryProvider(
Environment environment, Environment environment,
JacksonService jacksonService, JacksonService jacksonService,
BootLogger bootLogger, BootLogger bootLogger,
Set<OptionMetadata> optionMetadataSet, Set<OptionMetadata> optionMetadata,
Set<OptionRefWithConfig> optionDecorators,
Cli cli) { Cli cli) {


this.configurationSource = configurationSource; this.configurationSource = configurationSource;
this.environment = environment; this.environment = environment;
this.jacksonService = jacksonService; this.jacksonService = jacksonService;
this.bootLogger = bootLogger; this.bootLogger = bootLogger;
this.optionMetadataSet = optionMetadataSet; this.optionMetadata = optionMetadata;
this.optionDecorators = optionDecorators;
this.cli = cli; this.cli = cli;
} }


Expand Down Expand Up @@ -97,7 +102,7 @@ private Function<JsonNode, JsonNode> andCliOptionOverrider(
Function<URL, Optional<JsonNode>> parser, Function<URL, Optional<JsonNode>> parser,
BinaryOperator<JsonNode> singleConfigMerger) { BinaryOperator<JsonNode> singleConfigMerger) {


if (optionMetadataSet.isEmpty()) { if (optionMetadata.isEmpty()) {
return overrider; return overrider;
} }


Expand All @@ -106,20 +111,28 @@ private Function<JsonNode, JsonNode> andCliOptionOverrider(
return overrider; return overrider;
} }


List<URL> decoratorSources = optionDecorators.isEmpty() ? Collections.emptyList() : new ArrayList<>(5);

// options tied to config property paths // options tied to config property paths
HashMap<String, String> options = new HashMap<>(5); HashMap<String, String> options = new HashMap<>(5);


// options tied to config resources // options tied to config resources
List<URL> sources = new ArrayList<>(5); List<URL> optionSources = new ArrayList<>(5);


for (OptionSpec<?> cliOpt : detectedOptions) { for (OptionSpec<?> cliOpt : detectedOptions) {


OptionMetadata omd = findMetadata(cliOpt); OptionMetadata omd = findMetadata(cliOpt);


if(omd == null) { if (omd == null) {
continue; continue;
} }


for (OptionRefWithConfig decorator : optionDecorators) {
if (decorator.getOptionName().equals(omd.getName())) {
decoratorSources.add(decorator.getConfigResource().getUrl());
}
}

if (omd.getConfigPath() != null) { if (omd.getConfigPath() != null) {
String cliValue = cli.optionString(omd.getName()); String cliValue = cli.optionString(omd.getName());
if (cliValue == null) { if (cliValue == null) {
Expand All @@ -130,16 +143,22 @@ private Function<JsonNode, JsonNode> andCliOptionOverrider(
} }


if (omd.getConfigResource() != null) { if (omd.getConfigResource() != null) {
sources.add(omd.getConfigResource().getUrl()); optionSources.add(omd.getConfigResource().getUrl());
} }
} }


// config decorators are loaded first, and then can be overridden from options...
if(!decoratorSources.isEmpty()) {
overrider = overrider.andThen(new InPlaceResourceOverrider(decoratorSources, parser, singleConfigMerger));
}

if (!options.isEmpty()) { if (!options.isEmpty()) {
overrider = overrider.andThen(new InPlaceMapOverrider(options, true, '.')); overrider = overrider.andThen(new InPlaceMapOverrider(options, true, '.'));
} }


if (!sources.isEmpty()) { // deprecated...
overrider = overrider.andThen(new InPlaceResourceOverrider(sources, parser, singleConfigMerger)); if (!optionSources.isEmpty()) {
overrider = overrider.andThen(new InPlaceResourceOverrider(optionSources, parser, singleConfigMerger));
} }


return overrider; return overrider;
Expand All @@ -152,7 +171,7 @@ private OptionMetadata findMetadata(OptionSpec<?> option) {
// TODO: allow lookup of option metadata by name to avoid linear scans... // TODO: allow lookup of option metadata by name to avoid linear scans...
// Though we are dealing with small collection, so shouldn't be too horrible. // Though we are dealing with small collection, so shouldn't be too horrible.


for (OptionMetadata omd : optionMetadataSet) { for (OptionMetadata omd : optionMetadata) {
if (optionNames.contains(omd.getName())) { if (optionNames.contains(omd.getName())) {
return omd; return omd;
} }
Expand Down
29 changes: 29 additions & 0 deletions bootique/src/main/java/io/bootique/config/OptionRefWithConfig.java
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.bootique.config;

import io.bootique.resource.ResourceFactory;

/**
* @since 0.25
*/
public class OptionRefWithConfig {

private String optionName;
private String configResourceId;

public OptionRefWithConfig(String optionName, String configResourceId) {
this.optionName = optionName;
this.configResourceId = configResourceId;
}

public String getOptionName() {
return optionName;
}

public String getConfigResourceId() {
return configResourceId;
}

public ResourceFactory getConfigResource() {
return new ResourceFactory(configResourceId);
}
}
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ public class OptionMetadata implements MetadataNode {
private OptionValueCardinality valueCardinality; private OptionValueCardinality valueCardinality;
private String valueName; private String valueName;
private String configPath; private String configPath;
private ResourceFactory configResource;
private String defaultValue; private String defaultValue;


@Deprecated
private ResourceFactory configResource;

public static Builder builder(String name) { public static Builder builder(String name) {
return new Builder().name(name); return new Builder().name(name);
} }
Expand Down Expand Up @@ -71,7 +73,11 @@ public String getConfigPath() {
* *
* @return an optional resource associated with this option. * @return an optional resource associated with this option.
* @since 0.24 * @since 0.24
* @deprecated since 0.25. The new way of adding an option associated with a config file is by separately declaring
* an option and then associating it with one or more configs via
* {@link io.bootique.BQCoreModuleExtender#addConfigOnOption(String, String)}.
*/ */
@Deprecated
public ResourceFactory getConfigResource() { public ResourceFactory getConfigResource() {
return configResource; return configResource;
} }
Expand Down Expand Up @@ -169,6 +175,9 @@ public Builder defaultValue(String defaultValue) {
* a configuration source. E.g. "a/b/my.yml", or "classpath:com/foo/another.yml". * a configuration source. E.g. "a/b/my.yml", or "classpath:com/foo/another.yml".
* @return this builder instance * @return this builder instance
* @since 0.24 * @since 0.24
* @deprecated since 0.25. The new way of adding an option associated with a config file is by separately declaring
* an option and then associating it with one or more configs via
* {@link io.bootique.BQCoreModuleExtender#addConfigOnOption(String, String)}.
*/ */
public Builder configResource(String configResourceId) { public Builder configResource(String configResourceId) {
this.option.configResource = new ResourceFactory(configResourceId); this.option.configResource = new ResourceFactory(configResourceId);
Expand Down
51 changes: 49 additions & 2 deletions bootique/src/test/java/io/bootique/Bootique_ConfigurationIT.java
Original file line number Original file line Diff line number Diff line change
@@ -1,12 +1,14 @@
package io.bootique; package io.bootique;


import io.bootique.config.ConfigurationFactory; import io.bootique.config.ConfigurationFactory;
import io.bootique.meta.application.OptionMetadata;
import io.bootique.type.TypeRef; import io.bootique.type.TypeRef;
import io.bootique.unit.BQInternalTestFactory; import io.bootique.unit.BQInternalTestFactory;
import org.junit.Rule; import org.junit.Rule;
import org.junit.Test; import org.junit.Test;


import java.util.Map; import java.util.Map;
import java.util.function.Function;


import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
Expand Down Expand Up @@ -61,12 +63,57 @@ public void testDIConfig_VsCliOrder() {
.addConfig("classpath:io/bootique/diconfig2.yml")) .addConfig("classpath:io/bootique/diconfig2.yml"))
.createRuntime(); .createRuntime();


Map<String, String> config = runtime.getInstance(ConfigurationFactory.class) Map<String, Integer> config = runtime.getInstance(ConfigurationFactory.class)
.config(new TypeRef<Map<String, String>>() { .config(new TypeRef<Map<String, Integer>>() {
}, ""); }, "");
assertEquals("{a=5, b=2, c=6}", config.toString()); assertEquals("{a=5, b=2, c=6}", config.toString());
} }


@Test
public void testDIOnOptionConfig() {

Function<String, String> configReader =
arg -> {
BQRuntime runtime = runtimeFactory.app(arg)
.module(b -> BQCoreModule.extend(b)
.addConfigOnOption("opt", "classpath:io/bootique/diconfig1.yml")
.addConfigOnOption("opt", "classpath:io/bootique/diconfig2.yml")
.addOption(OptionMetadata.builder("opt").build()))
.createRuntime();

Map<String, Integer> config =
runtime.getInstance(ConfigurationFactory.class)
.config(new TypeRef<Map<String, Integer>>() {
}, "");

return config.toString();
};

assertEquals("{}", configReader.apply(""));
assertEquals("{a=1, b=2, c=3}", configReader.apply("--opt"));
}

@Test
public void testDIOnOptionConfig_OverrideWithOption() {

Function<String, String> configReader =
arg -> {
BQRuntime runtime = runtimeFactory.app(arg)
.module(b -> BQCoreModule.extend(b)
.addConfigOnOption("opt", "classpath:io/bootique/diconfig1.yml")
.addConfigOnOption("opt", "classpath:io/bootique/diconfig2.yml")
.addOption("a", "opt"))
.createRuntime();

return runtime.getInstance(ConfigurationFactory.class)
.config(new TypeRef<Map<String, Integer>>() {
}, "").toString();
};

assertEquals("{}", configReader.apply(""));
assertEquals("{a=8, b=2, c=3}", configReader.apply("--opt=8"));
}

@Test @Test
public void testConfigConfig() { public void testConfigConfig() {
BQRuntime runtime = runtimeFactory.app("--config=src/test/resources/io/bootique/test1.yml", BQRuntime runtime = runtimeFactory.app("--config=src/test/resources/io/bootique/test1.yml",
Expand Down
Loading

0 comments on commit a924172

Please sign in to comment.