From 5a0ee2dfb9b342759b7a24b7001ed11601030754 Mon Sep 17 00:00:00 2001 From: Roberto Cortez Date: Mon, 30 Mar 2020 12:36:21 +0100 Subject: [PATCH] Fixes #268. Added interceptor to Relocate configuration properties. --- .../ROOT/pages/interceptors/interceptors.adoc | 89 +++++++++++++++---- .../config/ConfigSourceInterceptor.java | 11 ++- .../ConfigSourceInterceptorContext.java | 6 +- .../ConfigSourceInterceptorFactory.java | 8 ++ .../FallbackConfigSourceInterceptor.java | 31 +++++++ .../RelocateConfigSourceInterceptor.java | 29 ++++++ .../config/SmallRyeConfigBuilder.java | 7 ++ .../smallrye/config/InterceptorChainTest.java | 52 +++++++++++ .../MappingConfigSourceInterceptorTest.java | 58 ++++++++++++ 9 files changed, 268 insertions(+), 23 deletions(-) create mode 100644 implementation/src/main/java/io/smallrye/config/FallbackConfigSourceInterceptor.java create mode 100644 implementation/src/main/java/io/smallrye/config/RelocateConfigSourceInterceptor.java create mode 100644 implementation/src/test/java/io/smallrye/config/InterceptorChainTest.java create mode 100644 implementation/src/test/java/io/smallrye/config/MappingConfigSourceInterceptorTest.java diff --git a/doc/modules/ROOT/pages/interceptors/interceptors.adoc b/doc/modules/ROOT/pages/interceptors/interceptors.adoc index baf5b3c93..16cea2e22 100644 --- a/doc/modules/ROOT/pages/interceptors/interceptors.adoc +++ b/doc/modules/ROOT/pages/interceptors/interceptors.adoc @@ -5,19 +5,19 @@ include::../attributes.adoc[] = Interceptors -SmallRye Config provides an interceptor chain that allows you to hook into the Configuration Value resolution. This is +SmallRye Config provides an interceptor chain that hooks into the Configuration Value resolution. This is useful to implement features like Property Substitution, Configuration Profiles, or just Logging to find out where the Config was loaded from. == Usage -You can create your interceptor by implementing the +An interceptor can be created by implementing the https://github.com/smallrye/smallrye-config/blob/master/implementation/src/main/java/io/smallrye/config/ConfigSourceInterceptor.java[ConfigSourceInterceptor] interface. The `ConfigSourceInterceptor` has a single method `ConfigValue getValue(ConfigSourceInterceptorContext context, String name)` -to intercept the resolution of a configuration key. You can use the `ConfigSourceInterceptorContext` to proceed with -the interceptor chain. The chain can be short-circuited by returning your instance of `ConfigValue`. +to intercept the resolution of a configuration key. The `ConfigSourceInterceptorContext` is used to proceed with +the interceptor chain. The chain can be short-circuited by returning an instance of `ConfigValue`. The `ConfigValue` objects hold information about the key name, value, config source origin and ordinal. @@ -25,10 +25,16 @@ The Interceptor Chain is applied before any Conversion is performed on the Confi === Registration -You can register an interceptor in the interceptor chain by using the `ServiceLoader` mechanism and provide the +Registration of an interceptor in the interceptor chain is done via the `ServiceLoader` mechanism and provide the implementation classes in a `META-INF/services/io.smallrye.config.ConfigSourceInterceptor` file. -Alternatively, you can register interceptors via the Programmatic API in `SmallRyeConfigBuilder#withInterceptors`. +Registration can also be done with a `ConfigSourceInterceptorFactory`, via the`ServiceLoader` mechanism +and provide the implementation class in a `META-INF/services/io.smallrye.config.ConfigSourceInterceptorFactory` file. +The `ConfigSourceInterceptorFactory` may initialize the interceptor with access to the current chain +(so it can be used to configure the interceptor and retrieve configuration values) and set the priority. + +Alternatively, interceptors may be registered via the Programmatic API in `SmallRyeConfigBuilder#withInterceptors` or +`SmallRyeConfigBuilder.withInterceptorFactories`. === Priority @@ -42,6 +48,12 @@ SmallRye Config provides the following built-in Interceptors: * <> * <> +* <> +* <> + +None of the interceptors is registered by default. Registration needs to happen via the ServiceLoader +mechanism, via the Programmatic API or by calling `SmallRyeConfigBuilder.addDefaultInterceptors`, which adds the +`ExpressionConfigSourceInterceptor` and the `ProfileConfigSourceInterceptor`. [[expression-interceptor]] === ExpressionConfigSourceInterceptor @@ -49,7 +61,7 @@ SmallRye Config provides the following built-in Interceptors: The `ExpressionConfigSourceInterceptor` provides expression expansion on Configuration Values. An expression string is a mix of plain strings and expression segments, which are wrapped by the sequence `${ ... }`. -For instance, if you have the following configuration: +For instance, the following configuration properties file: [source,properties] ---- @@ -67,17 +79,14 @@ Additionally, the Expression Expansion engine supports the following segments: If an expression cannot be expanded and no default is supplied a `NoSuchElementException` is thrown. -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 +The `ProfileConfigSourceInterceptor` allows multiple configurations with the same name and selects 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: +To be able to set properties with the same name, each property needs to be prefixed with `%` followed by the profile +name and a dot: [source,properties] ---- @@ -85,15 +94,59 @@ 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. +Lookup is always done using the `my.prop` property name. To use the profile `dev`, the configuration +`smallrye.config.profile=dev` has to be set 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. +Only one profile can be active at 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 +A ConfigSource with the highest priority, that defines `my.prop` will take priority over another low priority +ConfigSource that defines `%dev.my.prop`. This allows overriding profiles properties regardless of the active profile. + +[[relocate-interceptor]] +=== RelocateConfigSourceInterceptor + +The `RelocateConfigSourceInterceptor` allows relocating a configuration name to another name, by providing a +transformation function or just a simple key value map. + +Consider when a configuration key is renamed, lookup needs to happen on the new name, but also on the old name if the +ConfigSources are not updated yet. The relocation function gives priority to the new resolved configuration name or +resolves to the old name if no value is found under the new relocation name. + +The following `RelocateConfigSourceInterceptor` can relocate configuration names in the `smallrye.config` +namespace to the `microprofile.config` namespace: + +[source,java] +---- +new RelocateConfigSourceInterceptor( + name -> name.startsWith("smallrye.config") ? + name.replaceAll("smallrye\\.config", "microprofile.config") : + name)); +---- + +Relocation can also be done with Expression expansion. + +[[fallback-interceptor]] +=== FallbackConfigSourceInterceptor + +The `FallbackConfigSourceInterceptor` allows to fallback to another configuration name, by providing a transformation +function or just a simple key value map. + +Consider when a configuration name does not exist, but there might be another configuration name that the config can +fallback to provide the same expected behavior. The fallback function is only applied if the original resolved +configuration name is not found and resolved to the fallback name. + +The following `FallbackConfigSourceInterceptor` can fallback configuration names in the `microprofile.config` +namespace to the `smallrye.config` namespace: + +[source,java] +---- +new FallbackConfigSourceInterceptor( + name -> name.startsWith("microprofile.config") ? + name.replaceAll("microprofile\\.config", "smallrye.config") : + name)); +---- diff --git a/implementation/src/main/java/io/smallrye/config/ConfigSourceInterceptor.java b/implementation/src/main/java/io/smallrye/config/ConfigSourceInterceptor.java index 818c8571e..354e98b4b 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigSourceInterceptor.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigSourceInterceptor.java @@ -4,15 +4,18 @@ /** * The ConfigSourceInterceptor allows you to intercept the resolution of a configuration key name before the - * configuration value is resolved by the Config. + * configuration value is resolved by the Config and before any conversion taking place. + *

* * This is useful to provide logging, transform the key or substitute the value. + *

* - * Implementations of ConfigSourceInterceptor are loaded via the {@link java.util.ServiceLoader} mechanism and and can - * be registered by providing a resource named {@code META-INF/services/io.smallrye.config.ConfigSourceInterceptor}, + * Implementations of {@link ConfigSourceInterceptor} are loaded via the {@link java.util.ServiceLoader} mechanism and + * can be registered by providing a resource named {@code META-INF/services/io.smallrye.config.ConfigSourceInterceptor}, * which contains the fully qualified {@code ConfigSourceInterceptor} implementation class name as its content. + *

* - * A ConfigSourceInterceptor implementation class can specify a priority by way of the standard + * A {@link ConfigSourceInterceptor} implementation class can specify a priority by way of the standard * {@code javax.annotation.Priority} annotation. If no priority is explicitly assigned, the default priority value * of {@code 100} is assumed. If multiple interceptors are registered with the same priority, then their execution * order may be non deterministic. diff --git a/implementation/src/main/java/io/smallrye/config/ConfigSourceInterceptorContext.java b/implementation/src/main/java/io/smallrye/config/ConfigSourceInterceptorContext.java index c65f3a625..1cf6208f0 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigSourceInterceptorContext.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigSourceInterceptorContext.java @@ -2,12 +2,16 @@ import java.io.Serializable; +/** + * Exposes contextual information about the intercepted invocation of {@link ConfigSourceInterceptor}. This allows you + * to control the behavior of the invocation chain. + */ public interface ConfigSourceInterceptorContext extends Serializable { /** * Proceeds to the next interceptor. * * @param name the new key name to lookup. Can be the original key. - * @return a ConfigValue with information about the key, lookup value and source ConfigSource. + * @return a {@link ConfigValue} with information about the key, lookup value and source ConfigSource. */ ConfigValue proceed(String name); } diff --git a/implementation/src/main/java/io/smallrye/config/ConfigSourceInterceptorFactory.java b/implementation/src/main/java/io/smallrye/config/ConfigSourceInterceptorFactory.java index 928fc721e..4358b7edb 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigSourceInterceptorFactory.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigSourceInterceptorFactory.java @@ -5,10 +5,18 @@ /** * 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. + *

+ * + * Instances of this interface will be {@link SmallRyeConfigBuilder#addDiscoveredInterceptors()} via the + * {@link java.util.ServiceLoader} mechanism and can be registered by providing a + * {@code META-INF/services/io.smallrye.config.ConfigSourceInterceptorFactory} + * {@linkplain ClassLoader#getResource(String) resource} which contains the fully qualified class name of the + * custom {@code ConfigSourceProvider} implementation. */ public interface ConfigSourceInterceptorFactory { /** diff --git a/implementation/src/main/java/io/smallrye/config/FallbackConfigSourceInterceptor.java b/implementation/src/main/java/io/smallrye/config/FallbackConfigSourceInterceptor.java new file mode 100644 index 000000000..5fca63337 --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/FallbackConfigSourceInterceptor.java @@ -0,0 +1,31 @@ +package io.smallrye.config; + +import java.util.Map; +import java.util.function.Function; + +import javax.annotation.Priority; + +@Priority(300) +public class FallbackConfigSourceInterceptor implements ConfigSourceInterceptor { + private final Function mapping; + + public FallbackConfigSourceInterceptor(final Function mapping) { + this.mapping = mapping != null ? mapping : Function.identity(); + } + + public FallbackConfigSourceInterceptor(final Map mappings) { + this(name -> mappings.getOrDefault(name, name)); + } + + @Override + public ConfigValue getValue(final ConfigSourceInterceptorContext context, final String name) { + ConfigValue configValue = context.proceed(name); + if (configValue == null) { + final String map = mapping.apply(name); + if (!name.equals(map)) { + configValue = context.proceed(map); + } + } + return configValue; + } +} diff --git a/implementation/src/main/java/io/smallrye/config/RelocateConfigSourceInterceptor.java b/implementation/src/main/java/io/smallrye/config/RelocateConfigSourceInterceptor.java new file mode 100644 index 000000000..30805bf76 --- /dev/null +++ b/implementation/src/main/java/io/smallrye/config/RelocateConfigSourceInterceptor.java @@ -0,0 +1,29 @@ +package io.smallrye.config; + +import java.util.Map; +import java.util.function.Function; + +import javax.annotation.Priority; + +@Priority(400) +public class RelocateConfigSourceInterceptor implements ConfigSourceInterceptor { + private final Function mapping; + + public RelocateConfigSourceInterceptor(final Function mapping) { + this.mapping = mapping != null ? mapping : Function.identity(); + } + + public RelocateConfigSourceInterceptor(final Map mappings) { + this(name -> mappings.getOrDefault(name, name)); + } + + @Override + public ConfigValue getValue(final ConfigSourceInterceptorContext context, final String name) { + final String map = mapping.apply(name); + ConfigValue configValue = context.proceed(map); + if (configValue == null && !name.equals(map)) { + configValue = context.proceed(name); + } + return configValue; + } +} diff --git a/implementation/src/main/java/io/smallrye/config/SmallRyeConfigBuilder.java b/implementation/src/main/java/io/smallrye/config/SmallRyeConfigBuilder.java index 43ab3ea14..d92b57d65 100644 --- a/implementation/src/main/java/io/smallrye/config/SmallRyeConfigBuilder.java +++ b/implementation/src/main/java/io/smallrye/config/SmallRyeConfigBuilder.java @@ -178,6 +178,13 @@ public SmallRyeConfigBuilder withInterceptors(ConfigSourceInterceptor... interce return this; } + public SmallRyeConfigBuilder withInterceptorFactories(ConfigSourceInterceptorFactory... interceptorFactories) { + this.interceptors.addAll(Stream.of(interceptorFactories) + .map(InterceptorWithPriority::new) + .collect(Collectors.toList())); + return this; + } + @Override public SmallRyeConfigBuilder withConverters(Converter[] converters) { for (Converter converter : converters) { diff --git a/implementation/src/test/java/io/smallrye/config/InterceptorChainTest.java b/implementation/src/test/java/io/smallrye/config/InterceptorChainTest.java new file mode 100644 index 000000000..c13afcdc0 --- /dev/null +++ b/implementation/src/test/java/io/smallrye/config/InterceptorChainTest.java @@ -0,0 +1,52 @@ +package io.smallrye.config; + +import static io.smallrye.config.ProfileConfigSourceInterceptor.SMALLRYE_PROFILE; +import static org.junit.Assert.assertEquals; + +import java.util.OptionalInt; + +import org.eclipse.microprofile.config.Config; +import org.junit.Test; + +public class InterceptorChainTest { + @Test + public void chain() { + final Config config = buildConfig( + "my.prop", "1", // original property + "%prof.my.prop", "${%prof.my.prop.profile}", // profile property with expansion + "%prof.my.prop.profile", "2", + "my.prop.relocate", "3", // relocation + "%prof.my.prop.relocate", "4", // profile with relocation + SMALLRYE_PROFILE, "prof" // profile to use + ); + assertEquals("4", config.getValue("my.prop", String.class)); + } + + private static Config buildConfig(String... keyValues) { + return new SmallRyeConfigBuilder() + .addDefaultSources() + .addDefaultInterceptors() + .withSources(KeyValuesConfigSource.config(keyValues)) + .withInterceptors( + new RelocateConfigSourceInterceptor(s -> { + if (s.contains("my.prop.profile")) { + return "my.prop.relocate"; + } + return s; + })) + // Add the Profile Interceptor again because relocation may require a new search in the profiles + .withInterceptorFactories( + new ConfigSourceInterceptorFactory() { + @Override + public ConfigSourceInterceptor getInterceptor(final ConfigSourceInterceptorContext context) { + return new ProfileConfigSourceInterceptor(context); + } + + @Override + public OptionalInt getPriority() { + return OptionalInt.of(399); + } + }) + .build(); + } +} diff --git a/implementation/src/test/java/io/smallrye/config/MappingConfigSourceInterceptorTest.java b/implementation/src/test/java/io/smallrye/config/MappingConfigSourceInterceptorTest.java new file mode 100644 index 000000000..65c497c58 --- /dev/null +++ b/implementation/src/test/java/io/smallrye/config/MappingConfigSourceInterceptorTest.java @@ -0,0 +1,58 @@ +package io.smallrye.config; + +import static org.junit.Assert.assertEquals; + +import java.util.HashMap; + +import org.eclipse.microprofile.config.Config; +import org.junit.Test; + +public class MappingConfigSourceInterceptorTest { + @Test + public void relocateAndFallback() { + final Config config = buildConfig("mp.jwt.token.header", "Authorization", + "mp.jwt.token.cookie", "Bearer"); + + assertEquals("Authorization", config.getValue("smallrye.jwt.token.header", String.class)); + assertEquals("Bearer", config.getValue("smallrye.jwt.token.cookie", String.class)); + } + + @Test + public void relocate() { + final Config config = buildConfig( + "smallrye.jwt.token.header", "Cookie", + "mp.jwt.token.header", "Authorization"); + + assertEquals("Authorization", config.getValue("smallrye.jwt.token.header", String.class)); + } + + @Test + public void fallback() { + final Config config = buildConfig( + "smallrye.jwt.token.cookie", "jwt", + "mp.jwt.token.cookie", "Bearer"); + + assertEquals("jwt", config.getValue("smallrye.jwt.token.cookie", String.class)); + } + + private static Config buildConfig(String... keyValues) { + return new SmallRyeConfigBuilder() + .addDefaultSources() + .withSources(KeyValuesConfigSource.config(keyValues)) + .withInterceptors( + new RelocateConfigSourceInterceptor( + new HashMap() { + { + put("smallrye.jwt.token.header", "mp.jwt.token.header"); + } + }), + new FallbackConfigSourceInterceptor( + new HashMap() { + { + put("smallrye.jwt.token.header", "mp.jwt.token.header"); + put("smallrye.jwt.token.cookie", "mp.jwt.token.cookie"); + } + })) + .build(); + } +}