Skip to content

Commit

Permalink
Merge pull request helidon-io#509 from batsatt/se-map-env-vars_446
Browse files Browse the repository at this point in the history
Make SE and MP share config env var mapping
  • Loading branch information
batsatt authored Mar 19, 2019
2 parents f8802e9 + b29f121 commit 1053e7a
Show file tree
Hide file tree
Showing 10 changed files with 660 additions and 133 deletions.
7 changes: 6 additions & 1 deletion config/config/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,14 @@
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<environmentVariables>
<simple>unmapped-env-value</simple>
<_unmapped>unmapped-env-value</_unmapped>
<com_ACME_size>mapped-env-value</com_ACME_size>
<SERVER_EXECUTOR_dash_SERVICE_MAX_dash_POOL_dash_SIZE>mapped-env-value
</SERVER_EXECUTOR_dash_SERVICE_MAX_dash_POOL_dash_SIZE>
<CONFIG_SOURCE_TEST_PROPERTY>This Is My ENV VARS Value.</CONFIG_SOURCE_TEST_PROPERTY>
</environmentVariables>
</configuration>
</configuration>
</plugin>
</plugins>
</build>
Expand Down
79 changes: 48 additions & 31 deletions config/config/src/main/java/io/helidon/config/ConfigSources.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017, 2018 Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2017, 2019 Oracle and/or its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -26,7 +26,6 @@
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.concurrent.ScheduledExecutorService;
Expand All @@ -49,6 +48,8 @@
import io.helidon.config.spi.ConfigParser;
import io.helidon.config.spi.ConfigSource;

import static java.util.Objects.requireNonNull;

/**
* Provides access to built-in {@link ConfigSource} implementations.
*
Expand All @@ -57,6 +58,10 @@
public final class ConfigSources {

private static final String SOURCES_KEY = "sources";
static final String ENV_VARS_NAME = "env-vars";
static final String SYS_PROPS_NAME = "sys-props";
static final String DEFAULT_MAP_NAME = "map";
static final String DEFAULT_PROPERTIES_NAME = "properties";

private ConfigSources() {
throw new AssertionError("Instantiation not allowed.");
Expand Down Expand Up @@ -134,7 +139,7 @@ public static ConfigSource create(Readable readable, String mediaType) {
* <p>
* {@link Instant#now()} is the {@link ConfigParser.Content#stamp() content timestamp}.
*
* @param content a configuration content
* @param content a configuration content
* @param mediaType a configuration media type
* @return a config source
*/
Expand All @@ -156,7 +161,20 @@ public static ConfigSource create(String content, String mediaType) {
* @see #create(Properties)
*/
public static MapBuilder create(Map<String, String> map) {
return new MapBuilder(map);
return create(map, DEFAULT_MAP_NAME);
}

/**
* Provides a {@link MapBuilder} for creating a {@code ConfigSource}
* for a {@code Map}.
*
* @param map a map
* @param name the name.
* @return new Builder instance
* @see #create(Properties)
*/
public static MapBuilder create(Map<String, String> map, String name) {
return new MapBuilder(map, name);
}

/**
Expand All @@ -168,7 +186,20 @@ public static MapBuilder create(Map<String, String> map) {
* @see #create(Map)
*/
public static MapBuilder create(Properties properties) {
return new MapBuilder(properties);
return create(properties, DEFAULT_PROPERTIES_NAME);
}

/**
* Provides a {@link MapBuilder} for creating a {@code ConfigSource} for a
* {@code Map} from a {@code Properties} object.
*
* @param properties properties
* @param name the name.
* @return new Builder instance
* @see #create(Map)
*/
public static MapBuilder create(Properties properties, String name) {
return new MapBuilder(properties, name);
}

/**
Expand All @@ -190,7 +221,7 @@ public static ConfigSource prefixed(String key, Supplier<ConfigSource> sourceSup
* @return {@code ConfigSource} for config derived from system properties
*/
public static ConfigSource systemProperties() {
return create(System.getProperties()).lax().build();
return create(System.getProperties(), SYS_PROPS_NAME).lax().build();
}

/**
Expand All @@ -200,7 +231,7 @@ public static ConfigSource systemProperties() {
* @return {@code ConfigSource} for config derived from environment variables
*/
public static ConfigSource environmentVariables() {
return create(System.getenv()).lax().build();
return create(EnvironmentVariables.expand(), ENV_VARS_NAME).lax().build();
}

/**
Expand Down Expand Up @@ -416,41 +447,27 @@ public static CompositeBuilder load(Config metaConfig) {
* {@link ConfigSource#changes() ConfigSource mutability}.
*/
public static final class MapBuilder implements Builder<ConfigSource> {

private static final String PROPERTIES_NAME = "properties";
private static final String SYS_PROPS_NAME = "sys-props";
private static final String MAP_NAME = "map";
private static final String ENV_VARS_NAME = "env-vars";

private Map<String, String> map;
private boolean strict;
private String mapSourceName;
private volatile ConfigSource configSource;

private MapBuilder(Map<String, String> map) {
Objects.requireNonNull(map, "map cannot be null");
private MapBuilder(final Map<String, String> map, final String name) {
requireNonNull(name, "name cannot be null");
requireNonNull(map, "map cannot be null");

this.map = Collections.unmodifiableMap(map);
this.strict = true;
// mapSourceName
if (map == System.getenv()) {
mapSourceName = ENV_VARS_NAME;
} else {
mapSourceName = MAP_NAME;
}
this.mapSourceName = name;
}

private MapBuilder(Properties properties) {
Objects.requireNonNull(properties, "properties cannot be null");
private MapBuilder(final Properties properties, final String name) {
requireNonNull(name, "name cannot be null");
requireNonNull(properties, "properties cannot be null");

this.map = ConfigUtils.propertiesToMap(properties);
this.strict = true;
// mapSourceName
if (properties == System.getProperties()) {
mapSourceName = SYS_PROPS_NAME;
} else {
mapSourceName = PROPERTIES_NAME;
}
this.mapSourceName = name;
}

/**
Expand Down Expand Up @@ -568,7 +585,7 @@ private static List<ConfigSource> initConfigSources(List<Supplier<ConfigSource>>
* @return updated builder
*/
public CompositeBuilder add(Supplier<ConfigSource> source) {
Objects.requireNonNull(source, "source cannot be null");
requireNonNull(source, "source cannot be null");

configSources.add(source.get());
return this;
Expand All @@ -584,7 +601,7 @@ public CompositeBuilder add(Supplier<ConfigSource> source) {
* @see MergingStrategy#fallback()
*/
public CompositeBuilder mergingStrategy(MergingStrategy mergingStrategy) {
Objects.requireNonNull(mergingStrategy, "mergingStrategy cannot be null");
requireNonNull(mergingStrategy, "mergingStrategy cannot be null");

this.mergingStrategy = mergingStrategy;
return this;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/*
* Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package io.helidon.config;

import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;

/**
* Provides environment variables that include mapped variants enabling setting or overriding configuration with keys that are
* unlikely to be legal as environment variables.
* <p>
* The <a href="https://github.com/eclipse/microprofile-config/blob/master/spec/src/main/asciidoc/configsources.asciidoc">
* MP config specification</a> describes the environment variables {@code ConfigSource} as follows:
* <pre>
* Some operating systems allow only alphabetic characters or an underscore, _, in environment variables. Other
* characters such as ., /, etc may be disallowed. In order to set a value for a config property that has a name
* containing such disallowed characters from an environment variable, the following rules are used.
*
* This ConfigSource searches 3 environment variables for a given property name (e.g. com.ACME.size):
*
* 1. Exact match (i.e. com.ACME.size)
* 2. Replace the character that is neither alphanumeric nor _ with _ (i.e. com_ACME_size)
* 3. Replace the character that is neither alphanumeric nor _ with _ and convert to upper case (i.e. COM_ACME_SIZE)
*
* The first environment variable that is found is returned by this ConfigSource.
* </pre>
* <p>
* The spec assumes the mapping takes place during search, where the desired key is known, but Helidon merges
* {@code ConfigSource}s instead; therefore this implementation produces <em>additional</em> KV pairs with variant keys
* for any variable that can meaningfully be mapped. See {@link #shouldMap(String)} for the mapping criteria.
* <p>
* Since Helidon supports many configuration keys that contain {@code '-'} (e.g. {@code "server.executor-service.max-pool-size"}),
* an additional mapping is required to produce a matching variant. Given that it must map from legal environment variable names
* and reduce the chances of inadvertent mappings, a verbose mapping is used: {@code "_dash_"} substrings (lowercase only) are
* first replaced by {@code '-'}. See {@link #expand()} for the variants produced.
* <p>
*/
public class EnvironmentVariables {
private static final Pattern DASH_PATTERN = Pattern.compile("_dash_", Pattern.LITERAL);
private static final String UNDERSCORE = "_";
private static final String DOUBLE_UNDERSCORE = "__";
private static final String DASH = "-";
private static final char UNDERSCORE_CHAR = '_';
private static final char DOT_CHAR = '.';

/**
* Tests whether mappings should be created for the given environment variable name.
* <p>
* To provide a meaningful mapping, the name must meet <em>all</em> of the following criteria:
* <ol>
* <li>does not begin or end with a {@code '_'} character</li>
* <li>does not contain {@code "__"}</li>
* <li>contains one or more {@code '_'} characters</li>
* </ol>
*
* @param name The environment variable name.
* @return {@code true} if mappings should be created.
*/
public static boolean shouldMap(final String name) {
final int length = name.length();
return length > 2
&& name.charAt(0) != UNDERSCORE_CHAR
&& name.charAt(length - 1) != UNDERSCORE_CHAR
&& name.contains(UNDERSCORE)
&& !name.contains(DOUBLE_UNDERSCORE);
}

/**
* Returns the environment variables and their mapped variants.
* <p>
* The following mappings are applied to any environment variable name for which {@link #shouldMap(String)} returns
* {@code true}:
* <ol>
* <li>Replace {@code "_dash_"} by {@code '-'}, e.g. {@code "SERVER_EXECUTOR_dash_SERVICE_MAX_dash_POOL_dash_SIZE"} becomes
* {@code "SERVER_EXECUTOR-SERVICE_MAX-POOL-SIZE"}.</li>
* <li>Replace {@code '_'} by {@code '.'} and add as a variant, e.g. {@code "com_ACME_size"} becomes {@code "com.ACME.size"}
* and {@code "SERVER_EXECUTOR-SERVICE_MAX-POOL-SIZE"} becomes {@code "SERVER.EXECUTOR-SERVICE.MAX-POOL-SIZE"}. This mapping
* is added primarily to support mixed case config keys such as {@code "app.someCamelCaseKey"}.</li>
* <li>Convert the result of step 2 to lowercase and add as a variant, e.g. {@code "com.ACME.size"} becomes
* {@code "com.acme.size"} and {@code "SERVER.EXECUTOR-SERVICE.MAX-POOL-SIZE"} becomes
* {@code "server.executor-service.max-pool-size"}.
* </li>
* </ol>
*
* @return The map.
*/
public static Map<String, String> expand() {
return expand(System.getenv());
}

/**
* Returns the environment variables and their mapped variants.
* <p>
* The following mappings are applied to any environment variable name for which {@link #shouldMap(String)} returns
* {@code true}:
* <ol>
* <li>Replace {@code "_dash_"} by {@code '-'}, e.g. {@code "SERVER_EXECUTOR_dash_SERVICE_MAX_dash_POOL_dash_SIZE"} becomes
* {@code "SERVER_EXECUTOR-SERVICE_MAX-POOL-SIZE"}.</li>
* <li>Replace {@code '_'} by {@code '.'} and add as a variant, e.g. {@code "com_ACME_size"} becomes {@code "com.ACME.size"}
* and {@code "SERVER_EXECUTOR-SERVICE_MAX-POOL-SIZE"} becomes {@code "SERVER.EXECUTOR-SERVICE.MAX-POOL-SIZE"}. This mapping
* is added primarily to support mixed case config keys such as {@code "app.someCamelCaseKey"}.</li>
* <li>Convert the result of step 2 to lowercase and add as a variant, e.g. {@code "com.ACME.size"} becomes
* {@code "com.acme.size"} and {@code "SERVER.EXECUTOR-SERVICE.MAX-POOL-SIZE"} becomes
* {@code "server.executor-service.max-pool-size"}.
* </li>
* </ol>
*
* @param env The environment variables.
* @return A copy of {@code env} with variants added.
*/
public static Map<String, String> expand(final Map<String, String> env) {
final Map<String, String> result = new HashMap<>(env.size());
env.forEach((name, value) -> {
result.put(name, value);
if (shouldMap(name)) {
String alternateName = DASH_PATTERN.matcher(name).replaceAll(DASH);
alternateName = alternateName.replace(UNDERSCORE_CHAR, DOT_CHAR);
result.put(alternateName, value);
result.put(alternateName.toLowerCase(), value);
}
});

return result;
}

private EnvironmentVariables() {
}
}
Loading

0 comments on commit 1053e7a

Please sign in to comment.