Skip to content

Commit

Permalink
Fixes #268. Added interceptor to Relocate configuration properties. (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
radcortez authored Apr 15, 2020
1 parent cbad4a8 commit b21b84e
Show file tree
Hide file tree
Showing 9 changed files with 268 additions and 23 deletions.
89 changes: 71 additions & 18 deletions doc/modules/ROOT/pages/interceptors/interceptors.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,36 @@ 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.

The Interceptor Chain is applied before any Conversion is performed on the Configuration.

=== 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

Expand All @@ -42,14 +48,20 @@ SmallRye Config provides the following built-in Interceptors:

* <<expression-interceptor>>
* <<profile-interceptor>>
* <<relocate-interceptor>>
* <<fallback-interceptor>>

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

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]
----
Expand All @@ -67,33 +79,74 @@ 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]
----
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));
----
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
*
* This is useful to provide logging, transform the key or substitute the value.
* <p>
*
* 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.
* <p>
*
* 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,18 @@
/**
* This ConfigSourceInterceptorFactory allows to initialize a {@link ConfigSourceInterceptor}, with access to the
* current {@link ConfigSourceInterceptorContext}.
* <p>
*
* Interceptors in the chain are initialized in priority order and the current
* {@link ConfigSourceInterceptorContext} contains the current interceptor, plus all other interceptors already
* initialized.
* <p>
*
* 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 {
/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, String> mapping;

public FallbackConfigSourceInterceptor(final Function<String, String> mapping) {
this.mapping = mapping != null ? mapping : Function.identity();
}

public FallbackConfigSourceInterceptor(final Map<String, String> 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;
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String> mapping;

public RelocateConfigSourceInterceptor(final Function<String, String> mapping) {
this.mapping = mapping != null ? mapping : Function.identity();
}

public RelocateConfigSourceInterceptor(final Map<String, String> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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<String, String>() {
{
put("smallrye.jwt.token.header", "mp.jwt.token.header");
}
}),
new FallbackConfigSourceInterceptor(
new HashMap<String, String>() {
{
put("smallrye.jwt.token.header", "mp.jwt.token.header");
put("smallrye.jwt.token.cookie", "mp.jwt.token.cookie");
}
}))
.build();
}
}

0 comments on commit b21b84e

Please sign in to comment.