diff --git a/doc/modules/ROOT/pages/interceptors/interceptors.adoc b/doc/modules/ROOT/pages/interceptors/interceptors.adoc index 7592bcb46..baf5b3c93 100644 --- a/doc/modules/ROOT/pages/interceptors/interceptors.adoc +++ b/doc/modules/ROOT/pages/interceptors/interceptors.adoc @@ -41,6 +41,7 @@ interceptors are registered with the same priority, then their execution order m SmallRye Config provides the following built-in Interceptors: * <> +* <> [[expression-interceptor]] === ExpressionConfigSourceInterceptor @@ -68,3 +69,31 @@ If an expression cannot be expanded and no default is supplied a `NoSuchElementE The `ExpressionConfigSourceInterceptor` is not registered by default. You need to register it via the ServiceLoader mechanism with your application. + +[[profile-interceptor]] +=== ProfileConfigSourceInterceptor + +The `ProfileConfigSourceInterceptor` allows you to have multiple configurations with the same name and select them via +a profile property. + +To be able to set properties with the same name, you need to prefix each property with `%` followed by the profile name +and a dot: + +[source,properties] +---- +my.prop=1234 +%dev.my.prop=5678 +---- + +Lookup is always done using the `my.prop` property name. If you want to use the profile `dev`, you need to set the +configuration `smallrye.config.profile=dev` into any valid ConfigSource. + +When looking up the property `my.prop` with the `dev` profile active the value is `5678`. + +Only one profile can be active in any given time. + +==== Config Priority over Profiles + +A ConfigSource with a highest priority, that defines `my.prop` will take priority over another low priority +ConfigSource that defines `%dev.my.prop`. This allows you to override profiles properties regardless of the active +profile. diff --git a/implementation/src/main/java/io/smallrye/config/ConfigSourceInterceptorFactory.java b/implementation/src/main/java/io/smallrye/config/ConfigSourceInterceptorFactory.java new file mode 100644 index 000000000..928fc721e --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/ConfigSourceInterceptorFactory.java @@ -0,0 +1,37 @@ +package io.smallrye.config; + +import java.util.OptionalInt; + +/** + * This ConfigSourceInterceptorFactory allows to initialize a {@link ConfigSourceInterceptor}, with access to the + * current {@link ConfigSourceInterceptorContext}. + * + * Interceptors in the chain are initialized in priority order and the current + * {@link ConfigSourceInterceptorContext} contains the current interceptor, plus all other interceptors already + * initialized. + */ +public interface ConfigSourceInterceptorFactory { + /** + * The default priority value, {@code 100}. + */ + int DEFAULT_PRIORITY = 100; + + /** + * Gets the {@link ConfigSourceInterceptor} from the ConfigSourceInterceptorFactory. Implementations of this + * method must provide the instance of the {@link ConfigSourceInterceptor} to add into the Config Interceptor Chain. + * + * @param context the current {@link ConfigSourceInterceptorContext} with the interceptors already initialized. + * @return the {@link ConfigSourceInterceptor} to add to Config Interceptor Chain and initialize. + */ + ConfigSourceInterceptor getInterceptor(final ConfigSourceInterceptorContext context); + + /** + * Returns the interceptor priority. This is required, because the interceptor priority needs to be sorted + * before doing initialization. + * + * @return the priority value. + */ + default OptionalInt getPriority() { + return OptionalInt.empty(); + } +} diff --git a/implementation/src/main/java/io/smallrye/config/ProfileConfigSourceInterceptor.java b/implementation/src/main/java/io/smallrye/config/ProfileConfigSourceInterceptor.java new file mode 100644 index 000000000..796c24f6b --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/ProfileConfigSourceInterceptor.java @@ -0,0 +1,57 @@ +package io.smallrye.config; + +import java.util.Comparator; +import java.util.Optional; + +import javax.annotation.Priority; + +@Priority(600) +public class ProfileConfigSourceInterceptor implements ConfigSourceInterceptor { + public static final String SMALLRYE_PROFILE = "smallrye.config.profile"; + + private static final Comparator CONFIG_SOURCE_COMPARATOR = (o1, o2) -> { + int res = Integer.compare(o2.getConfigSourceOrdinal(), o1.getConfigSourceOrdinal()); + if (res != 0) { + return res; + } + + if (o1.getConfigSourceName() != null && o2.getConfigSourceName() != null) { + return o2.getConfigSourceName().compareTo(o1.getConfigSourceName()); + } else { + return res; + } + }; + + private final String profile; + + public ProfileConfigSourceInterceptor(final String profile) { + this.profile = profile; + } + + public ProfileConfigSourceInterceptor(final ConfigSourceInterceptorContext context) { + this(context, SMALLRYE_PROFILE); + } + + public ProfileConfigSourceInterceptor( + final ConfigSourceInterceptorContext context, + final String profileConfigName) { + this.profile = Optional.ofNullable(context.proceed(profileConfigName)).map(ConfigValue::getValue).orElse(null); + } + + @Override + public ConfigValue getValue(final ConfigSourceInterceptorContext context, final String name) { + if (profile != null) { + final ConfigValue profileValue = context.proceed("%" + profile + "." + name); + if (profileValue != null) { + final ConfigValue originalValue = context.proceed(name); + if (originalValue != null && CONFIG_SOURCE_COMPARATOR.compare(profileValue, originalValue) > 0) { + return originalValue; + } else { + return profileValue; + } + } + } + + return context.proceed(name); + } +} diff --git a/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java b/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java index d7f84e939..ae530ddab 100644 --- a/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java +++ b/implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java @@ -40,12 +40,12 @@ import java.util.function.IntFunction; import java.util.function.UnaryOperator; -import javax.annotation.Priority; - import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.spi.ConfigSource; import org.eclipse.microprofile.config.spi.Converter; +import io.smallrye.config.SmallRyeConfigBuilder.InterceptorWithPriority; + /** * @author Jeff Mesnil (c) 2017 Red Hat inc. */ @@ -125,19 +125,15 @@ private Map> buildConverters(final SmallRyeConfigBuilder buil } private ConfigSourceInterceptorContext buildInterceptorChain(final SmallRyeConfigBuilder builder) { - final List interceptors = new ArrayList<>(builder.getInterceptors()); + final List interceptors = new ArrayList<>(builder.getInterceptors()); if (builder.isAddDiscoveredInterceptors()) { interceptors.addAll(builder.discoverInterceptors()); } + if (builder.isAddDefaultInterceptors()) { + interceptors.addAll(builder.getDefaultInterceptors()); + } - interceptors.sort((o1, o2) -> { - final Integer p1 = Optional.ofNullable(o1.getClass().getAnnotation(Priority.class)).map(Priority::value) - .orElse(100); - final Integer p2 = Optional.ofNullable(o2.getClass().getAnnotation(Priority.class)).map(Priority::value) - .orElse(100); - - return Integer.compare(p2, p1); - }); + interceptors.sort(Comparator.comparingInt(InterceptorWithPriority::getPriority).reversed()); SmallRyeConfigSourceInterceptorContext current = new SmallRyeConfigSourceInterceptorContext( (ConfigSourceInterceptor) (context, name) -> { @@ -164,7 +160,7 @@ private ConfigSourceInterceptorContext buildInterceptorChain(final SmallRyeConfi }, null); for (int i = interceptors.size() - 1; i >= 0; i--) { - current = new SmallRyeConfigSourceInterceptorContext(interceptors.get(i), current); + current = new SmallRyeConfigSourceInterceptorContext(interceptors.get(i).getInterceptor(current), current); } return current; diff --git a/implementation/src/main/java/io/smallrye/config/SmallRyeConfigBuilder.java b/implementation/src/main/java/io/smallrye/config/SmallRyeConfigBuilder.java index 4355f7050..43ab3ea14 100644 --- a/implementation/src/main/java/io/smallrye/config/SmallRyeConfigBuilder.java +++ b/implementation/src/main/java/io/smallrye/config/SmallRyeConfigBuilder.java @@ -23,9 +23,13 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; import java.util.ServiceLoader; import java.util.function.Function; import java.util.function.UnaryOperator; +import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.annotation.Priority; @@ -46,9 +50,10 @@ public class SmallRyeConfigBuilder implements ConfigBuilder { private List sources = new ArrayList<>(); private Function sourceWrappers = UnaryOperator.identity(); private Map converters = new HashMap<>(); - private List interceptors = new ArrayList<>(); + private List interceptors = new ArrayList<>(); private ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); private boolean addDefaultSources = false; + private boolean addDefaultInterceptors = false; private boolean addDiscoveredSources = false; private boolean addDiscoveredConverters = false; private boolean addDiscoveredInterceptors = false; @@ -93,11 +98,16 @@ List discoverConverters() { return discoveredConverters; } - List discoverInterceptors() { - List interceptors = new ArrayList<>(); + List discoverInterceptors() { + List interceptors = new ArrayList<>(); ServiceLoader interceptorLoader = ServiceLoader.load(ConfigSourceInterceptor.class, classLoader); - interceptorLoader.forEach(interceptors::add); + interceptorLoader.forEach(interceptor -> interceptors.add(new InterceptorWithPriority(interceptor))); + + ServiceLoader interceptorFactoryLoader = ServiceLoader + .load(ConfigSourceInterceptorFactory.class, classLoader); + interceptorFactoryLoader.forEach(interceptor -> interceptors.add(new InterceptorWithPriority(interceptor))); + return interceptors; } @@ -120,6 +130,30 @@ List getDefaultSources() { return defaultSources; } + public SmallRyeConfigBuilder addDefaultInterceptors() { + this.addDefaultInterceptors = true; + return this; + } + + List getDefaultInterceptors() { + final List interceptors = new ArrayList<>(); + + interceptors.add(new InterceptorWithPriority(new ExpressionConfigSourceInterceptor())); + interceptors.add(new InterceptorWithPriority(new ConfigSourceInterceptorFactory() { + @Override + public ConfigSourceInterceptor getInterceptor(final ConfigSourceInterceptorContext context) { + return new ProfileConfigSourceInterceptor(context); + } + + @Override + public OptionalInt getPriority() { + return OptionalInt.of(600); + } + })); + + return interceptors; + } + @Override public SmallRyeConfigBuilder forClassLoader(ClassLoader classLoader) { this.classLoader = classLoader; @@ -138,7 +172,9 @@ public SmallRyeConfigBuilder withSources(Collection configSources) } public SmallRyeConfigBuilder withInterceptors(ConfigSourceInterceptor... interceptors) { - Collections.addAll(this.interceptors, interceptors); + this.interceptors.addAll(Stream.of(interceptors) + .map(InterceptorWithPriority::new) + .collect(Collectors.toList())); return this; } @@ -203,7 +239,7 @@ Map getConverters() { return converters; } - List getInterceptors() { + List getInterceptors() { return interceptors; } @@ -211,6 +247,10 @@ boolean isAddDefaultSources() { return addDefaultSources; } + boolean isAddDefaultInterceptors() { + return addDefaultInterceptors; + } + boolean isAddDiscoveredSources() { return addDiscoveredSources; } @@ -245,4 +285,46 @@ int getPriority() { return priority; } } + + static class InterceptorWithPriority { + private final ConfigSourceInterceptorFactory factory; + private final int priority; + + private InterceptorWithPriority(ConfigSourceInterceptor interceptor) { + this(new ConfigSourceInterceptorFactory() { + @Override + public ConfigSourceInterceptor getInterceptor(final ConfigSourceInterceptorContext context) { + return interceptor; + } + + @Override + public OptionalInt getPriority() { + final OptionalInt priority = ConfigSourceInterceptorFactory.super.getPriority(); + if (priority.isPresent()) { + return priority; + } + + final Integer annotationPriorityOrDefault = Optional + .ofNullable(interceptor.getClass().getAnnotation(Priority.class)) + .map(Priority::value) + .orElse(ConfigSourceInterceptorFactory.DEFAULT_PRIORITY); + + return OptionalInt.of(annotationPriorityOrDefault); + } + }); + } + + private InterceptorWithPriority(ConfigSourceInterceptorFactory factory) { + this.factory = factory; + this.priority = factory.getPriority().orElse(ConfigSourceInterceptorFactory.DEFAULT_PRIORITY); + } + + public ConfigSourceInterceptor getInterceptor(ConfigSourceInterceptorContext context) { + return factory.getInterceptor(context); + } + + public int getPriority() { + return priority; + } + } } diff --git a/implementation/src/test/java/io/smallrye/config/ConfigSourceInterceptorTest.java b/implementation/src/test/java/io/smallrye/config/ConfigSourceInterceptorTest.java index ab7de299b..3a84daa06 100644 --- a/implementation/src/test/java/io/smallrye/config/ConfigSourceInterceptorTest.java +++ b/implementation/src/test/java/io/smallrye/config/ConfigSourceInterceptorTest.java @@ -1,5 +1,8 @@ package io.smallrye.config; +import static io.smallrye.config.ProfileConfigSourceInterceptor.SMALLRYE_PROFILE; +import static org.junit.Assert.assertEquals; + import javax.annotation.Priority; import org.jboss.logging.Logger; @@ -37,11 +40,11 @@ public void priority() { public void serviceLoader() { SmallRyeConfig config = new SmallRyeConfigBuilder() .addDefaultSources() - .withSources(KeyValuesConfigSource.config("my.prop", "1234")) + .withSources(KeyValuesConfigSource.config("my.prop.loader", "1234")) .addDiscoveredInterceptors() .build(); - final String value = config.getValue("my.prop", String.class); + final String value = config.getValue("my.prop.loader", String.class); Assert.assertEquals("loader", value); } @@ -49,16 +52,30 @@ public void serviceLoader() { public void serviceLoaderAndPriorities() { SmallRyeConfig config = new SmallRyeConfigBuilder() .addDefaultSources() - .withSources(KeyValuesConfigSource.config("my.prop", "1234")) + .withSources(KeyValuesConfigSource.config("my.prop.loader", "1234")) .addDiscoveredInterceptors() .withInterceptors(new LowerPriorityConfigSourceInterceptor(), new HighPriorityConfigSourceInterceptor()) .build(); - final String value = config.getValue("my.prop", String.class); + final String value = config.getValue("my.prop.loader", String.class); Assert.assertEquals("higher", value); } + @Test + public void defaultInterceptors() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .addDefaultSources() + .withSources(KeyValuesConfigSource.config("my.prop", "1", + "%prof.my.prop", "${%prof.my.prop.profile}", + "%prof.my.prop.profile", "2", + SMALLRYE_PROFILE, "prof")) + .addDefaultInterceptors() + .build(); + + assertEquals("2", config.getValue("my.prop", String.class)); + } + private static class LoggingConfigSourceInterceptor implements ConfigSourceInterceptor { private static final Logger LOG = Logger.getLogger("io.smallrye.config"); diff --git a/implementation/src/test/java/io/smallrye/config/ConfigSourceProfileInterceptorTest.java b/implementation/src/test/java/io/smallrye/config/ConfigSourceProfileInterceptorTest.java deleted file mode 100644 index 2d243706b..000000000 --- a/implementation/src/test/java/io/smallrye/config/ConfigSourceProfileInterceptorTest.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.smallrye.config; - -import org.eclipse.microprofile.config.Config; -import org.junit.Assert; -import org.junit.Test; - -public class ConfigSourceProfileInterceptorTest { - @Test - public void interceptor() { - SmallRyeConfig config = (SmallRyeConfig) buildConfig("my.prop", "1234", "%prod.my.prop", "prod"); - - final String value = config.getValue("my.prop", String.class); - Assert.assertEquals("prod", value); - } - - private static Config buildConfig(String... keyValues) { - return new SmallRyeConfigBuilder() - .addDefaultSources() - .withSources(KeyValuesConfigSource.config(keyValues)) - .withInterceptors(new ProfileConfigSourceInterceptor()) - .build(); - } - - private static class ProfileConfigSourceInterceptor implements ConfigSourceInterceptor { - private final String profile = "prod"; - - @Override - public ConfigValue getValue(final ConfigSourceInterceptorContext context, final String name) { - ConfigValue configValue = context.proceed("%" + profile + "." + name); - if (configValue == null) { - configValue = context.proceed(name); - } - return configValue; - } - } -} diff --git a/implementation/src/test/java/io/smallrye/config/ProfileConfigSourceInterceptorTest.java b/implementation/src/test/java/io/smallrye/config/ProfileConfigSourceInterceptorTest.java new file mode 100644 index 000000000..0f6038bc2 --- /dev/null +++ b/implementation/src/test/java/io/smallrye/config/ProfileConfigSourceInterceptorTest.java @@ -0,0 +1,166 @@ +package io.smallrye.config; + +import static io.smallrye.config.ProfileConfigSourceInterceptor.SMALLRYE_PROFILE; +import static org.junit.Assert.assertEquals; + +import java.util.HashMap; + +import org.eclipse.microprofile.config.Config; +import org.junit.Test; + +import io.smallrye.config.common.MapBackedConfigSource; + +public class ProfileConfigSourceInterceptorTest { + @Test + public void profile() { + final Config config = buildConfig("my.prop", "1", "%prof.my.prop", "2", SMALLRYE_PROFILE, "prof"); + + assertEquals("2", config.getValue("my.prop", String.class)); + } + + @Test + public void profileOnly() { + final Config config = buildConfig("my.prop", "1", "%prof.my.prop", "2", SMALLRYE_PROFILE, "prof"); + + assertEquals("2", config.getValue("my.prop", String.class)); + } + + @Test + public void fallback() { + final Config config = buildConfig("my.prop", "1", SMALLRYE_PROFILE, "prof"); + + assertEquals("1", config.getValue("my.prop", String.class)); + } + + @Test + public void expressions() { + final Config config = buildConfig("my.prop", "1", "%prof.my.prop", "${my.prop}", SMALLRYE_PROFILE, "prof"); + + assertEquals("1", config.getValue("my.prop", String.class)); + } + + @Test + public void profileExpressions() { + final Config config = buildConfig("my.prop", "1", + "%prof.my.prop", "${%prof.my.prop.profile}", + "%prof.my.prop.profile", "2", + SMALLRYE_PROFILE, "prof"); + + assertEquals("2", config.getValue("my.prop", String.class)); + } + + @Test + public void customConfigProfile() { + final String[] configs = { "my.prop", "1", "%prof.my.prop", "2", "config.profile", "prof" }; + final Config config = new SmallRyeConfigBuilder() + .addDefaultSources() + .addDiscoveredInterceptors() + .withSources(KeyValuesConfigSource.config(configs)) + .build(); + + assertEquals("2", config.getValue("my.prop", String.class)); + } + + @Test + public void noConfigProfile() { + final String[] configs = { "my.prop", "1", "%prof.my.prop", "2" }; + final Config config = new SmallRyeConfigBuilder() + .addDefaultSources() + .withSources(KeyValuesConfigSource.config(configs)) + .withInterceptors( + new ProfileConfigSourceInterceptor("prof"), + new ExpressionConfigSourceInterceptor()) + .build(); + + assertEquals("2", config.getValue("my.prop", String.class)); + } + + @Test + public void priorityProfile() { + final Config config = new SmallRyeConfigBuilder() + .addDefaultSources() + .withSources( + new MapBackedConfigSource("higher", new HashMap() { + { + put("%prof.my.prop", "higher-profile"); + } + }, 200) { + }, + new MapBackedConfigSource("lower", new HashMap() { + { + put("my.prop", "lower"); + put("%prof.my.prop", "lower-profile"); + } + }, 100) { + }) + .withInterceptors( + new ProfileConfigSourceInterceptor("prof"), + new ExpressionConfigSourceInterceptor()) + .build(); + + assertEquals("higher-profile", config.getValue("my.prop", String.class)); + } + + @Test + public void priorityOverrideProfile() { + final Config config = new SmallRyeConfigBuilder() + .addDefaultSources() + .withSources( + new MapBackedConfigSource("higher", new HashMap() { + { + put("my.prop", "higher"); + } + }, 200) { + }, + new MapBackedConfigSource("lower", new HashMap() { + { + put("my.prop", "lower"); + put("%prof.my.prop", "lower-profile"); + } + }, 100) { + }) + .withInterceptors( + new ProfileConfigSourceInterceptor("prof"), + new ExpressionConfigSourceInterceptor()) + .build(); + + assertEquals("higher", config.getValue("my.prop", String.class)); + } + + @Test + public void priorityProfileOverOriginal() { + final Config config = new SmallRyeConfigBuilder() + .addDefaultSources() + .withSources( + new MapBackedConfigSource("higher", new HashMap() { + { + put("my.prop", "higher"); + put("%prof.my.prop", "higher-profile"); + } + }, 200) { + }, + new MapBackedConfigSource("lower", new HashMap() { + { + put("my.prop", "lower"); + put("%prof.my.prop", "lower-profile"); + } + }, 100) { + }) + .withInterceptors( + new ProfileConfigSourceInterceptor("prof"), + new ExpressionConfigSourceInterceptor()) + .build(); + + assertEquals("higher-profile", config.getValue("my.prop", String.class)); + } + + private static Config buildConfig(String... keyValues) { + return new SmallRyeConfigBuilder() + .addDefaultSources() + .withSources(KeyValuesConfigSource.config(keyValues)) + .withInterceptors( + new ProfileConfigSourceInterceptor("prof"), + new ExpressionConfigSourceInterceptor()) + .build(); + } +} diff --git a/implementation/src/test/java/io/smallrye/config/ServiceLoaderConfigSourceInterceptor.java b/implementation/src/test/java/io/smallrye/config/ServiceLoaderConfigSourceInterceptor.java index 7d5856c5c..8d9170380 100644 --- a/implementation/src/test/java/io/smallrye/config/ServiceLoaderConfigSourceInterceptor.java +++ b/implementation/src/test/java/io/smallrye/config/ServiceLoaderConfigSourceInterceptor.java @@ -3,6 +3,9 @@ public class ServiceLoaderConfigSourceInterceptor implements ConfigSourceInterceptor { @Override public ConfigValue getValue(final ConfigSourceInterceptorContext context, final String name) { - return ConfigValue.builder().withName(name).withValue("loader").build(); + if ("my.prop.loader".equals(name)) { + return ConfigValue.builder().withName(name).withValue("loader").build(); + } + return context.proceed(name); } } diff --git a/implementation/src/test/java/io/smallrye/config/ServiceLoaderConfigSourceInterceptorFactory.java b/implementation/src/test/java/io/smallrye/config/ServiceLoaderConfigSourceInterceptorFactory.java new file mode 100644 index 000000000..1c06a80c7 --- /dev/null +++ b/implementation/src/test/java/io/smallrye/config/ServiceLoaderConfigSourceInterceptorFactory.java @@ -0,0 +1,8 @@ +package io.smallrye.config; + +public class ServiceLoaderConfigSourceInterceptorFactory implements ConfigSourceInterceptorFactory { + @Override + public ConfigSourceInterceptor getInterceptor(final ConfigSourceInterceptorContext context) { + return new ProfileConfigSourceInterceptor(context, "config.profile"); + } +} diff --git a/implementation/src/test/resources/META-INF/services/io.smallrye.config.ConfigSourceInterceptorFactory b/implementation/src/test/resources/META-INF/services/io.smallrye.config.ConfigSourceInterceptorFactory new file mode 100644 index 000000000..fe460f58b --- /dev/null +++ b/implementation/src/test/resources/META-INF/services/io.smallrye.config.ConfigSourceInterceptorFactory @@ -0,0 +1 @@ +io.smallrye.config.ServiceLoaderConfigSourceInterceptorFactory