Skip to content

Commit

Permalink
Support Hocon/Json Configuration Source for MP (#4218)
Browse files Browse the repository at this point in the history
* Support Hocon/Json Configuration Source for MP

1. Meta-config is refactored to allow it to be customizable by users for other types.
2. Hocon/Json is parsed as a config source only via meta-config.

* Create hocon includer per instance of hocon config source and add justification comment for security bug filtering

* Remove support of application.conf/json hocon/json as default discoverable sources

* Initial commit for Mp config-source provider implementation

* Add MP meta-config provider test for Environment Variables

* Add Yaml MP meta-config provider tests and various resolution to review feedback

* Add helidon-config-yaml-mp as a dependency of helidon-microprofile-cdi

* Declare "uses io.helidon.config.mp.spi.MpMetaConfigProvider" in config-mp’s module-info and set priority of all implementations of MpMetaConfigProvider to 300

* Various updates/cleanups from review feedback

* Add helidon-config-yaml dependency to metrics examples after they were failing because of dependency on yaml-mp
  • Loading branch information
klustria authored May 20, 2022
1 parent 1f6c10e commit 84451b4
Show file tree
Hide file tree
Showing 80 changed files with 2,976 additions and 231 deletions.
5 changes: 5 additions & 0 deletions bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,11 @@
<artifactId>helidon-config-yaml-mp</artifactId>
<version>${helidon.version}</version>
</dependency>
<dependency>
<groupId>io.helidon.config</groupId>
<artifactId>helidon-config-hocon-mp</artifactId>
<version>${helidon.version}</version>
</dependency>
<dependency>
<groupId>io.helidon.config</groupId>
<artifactId>helidon-config-git</artifactId>
Expand Down
4 changes: 0 additions & 4 deletions config/config-mp/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,6 @@
<groupId>org.eclipse.microprofile.config</groupId>
<artifactId>microprofile-config-api</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.config</groupId>
<artifactId>helidon-config-yaml-mp</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
Expand Down
240 changes: 49 additions & 191 deletions config/config-mp/src/main/java/io/helidon/config/mp/MpConfigBuilder.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2020, 2021 Oracle and/or its affiliates.
* Copyright (c) 2020, 2022 Oracle and/or its affiliates.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -24,7 +24,6 @@
import java.net.URI;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
Expand All @@ -47,15 +46,14 @@
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.ServiceLoader;
import java.util.SimpleTimeZone;
import java.util.TimeZone;
import java.util.UUID;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
Expand All @@ -66,7 +64,7 @@
import io.helidon.config.ConfigMappers;
import io.helidon.config.ConfigValue;
import io.helidon.config.mp.spi.MpConfigFilter;
import io.helidon.config.yaml.mp.YamlMpConfigSource;
import io.helidon.config.mp.spi.MpMetaConfigProvider;

import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.spi.ConfigBuilder;
Expand All @@ -79,11 +77,31 @@
/**
* Configuration builder.
*/
@Deprecated
public class MpConfigBuilder implements ConfigBuilder {
class MpConfigBuilder implements ConfigBuilder {
private static final Logger LOGGER = Logger.getLogger(MpConfigBuilder.class.getName());
private static final String DEFAULT_CONFIG_SOURCE = "META-INF/microprofile-config.properties";

private static final Map<String, MpMetaConfigProvider> MP_META_PROVIDERS;
static {
List<MpMetaConfigProvider> mpMetaConfigProviders =
HelidonServiceLoader.builder(ServiceLoader.load(MpMetaConfigProvider.class))
.addService(new MpEnvironmentVariablesMetaConfigProvider())
.addService(new MpSystemPropertiesMetaConfigProvider())
.addService(new MpPropertiesMetaConfigProvider())
.build()
.asList();

Map<String, MpMetaConfigProvider> theMap = new HashMap<>();
// ordered by priority
for (MpMetaConfigProvider mpMetaConfigProvider : mpMetaConfigProviders) {
for (String supportedType : mpMetaConfigProvider.supportedTypes()) {
theMap.putIfAbsent(supportedType, mpMetaConfigProvider);
}
}
MP_META_PROVIDERS = Map.copyOf(theMap);
}


private final List<OrdinalSource> sources = new LinkedList<>();
private final List<OrdinalConverter> converters = new LinkedList<>();

Expand Down Expand Up @@ -154,14 +172,6 @@ private static Class<?> doGetType(Class<?> clazz) {
return doGetType(clazz.getSuperclass());
}

private static String toProfileName(String fileName, String profile) {
int i = fileName.lastIndexOf('.');
if (i > -1) {
return fileName.substring(0, i) + "-" + profile + fileName.substring(i);
}
return fileName + "-" + profile;
}

@Override
public ConfigBuilder addDefaultSources() {
useDefaultSources = true;
Expand Down Expand Up @@ -237,25 +247,14 @@ private void processMetaSources(List<io.helidon.config.Config> configs) {
for (io.helidon.config.Config config : configs) {
String type = config.get("type").asString()
.orElseThrow(() -> new ConfigException("Meta configuration sources must have a \"type\" property defined"));
// in MP, we have a hardcoded list of supported configuration source types
List<ConfigSource> delegates;
switch (type) {
case "system-properties":
delegates = List.of(MpConfigSources.systemProperties());
break;
case "environment-variables":
delegates = List.of(MpConfigSources.environmentVariables());
break;
case "properties":
delegates = propertiesSource(config);
break;
case "yaml":
delegates = yamlSource(config);
break;
default:
throw new ConfigException("Meta configuration source type \"" + type + "\" is not supported. Use on of: "
+ "system-properties, environment-variables, properties, yaml");
MpMetaConfigProvider mpMetaConfigProvider = MP_META_PROVIDERS.get(type);
if (mpMetaConfigProvider == null) {
throw new ConfigException("Wrong meta configuration, type " + type
+ " not supported, only supporting: " + MP_META_PROVIDERS.keySet());
}

List<? extends ConfigSource> delegates = mpMetaConfigProvider.create(type, config, profile);

boolean shouldCount = delegates.size() > 1;
int counter = 0;

Expand All @@ -279,163 +278,6 @@ private void processMetaSources(List<io.helidon.config.Config> configs) {
}
}

private List<ConfigSource> propertiesSource(io.helidon.config.Config config) {
return sourceFromMeta(config,
MpConfigSources::create,
MpConfigSources::classPath,
MpConfigSources::classPath,
MpConfigSources::create);
}

private List<ConfigSource> yamlSource(io.helidon.config.Config config) {
return sourceFromMeta(config,
YamlMpConfigSource::create,
YamlMpConfigSource::classPath,
YamlMpConfigSource::classPath,
YamlMpConfigSource::create);
}

private List<ConfigSource> sourceFromMeta(io.helidon.config.Config config,
Function<Path, ConfigSource> fromPath,
Function<String, List<ConfigSource>> fromClasspath,
BiFunction<String, String, List<ConfigSource>> fromClasspathWithProfile,
Function<URL, ConfigSource> fromUrl) {

boolean optional = config.get("optional").asBoolean().orElse(false);

String location;
Exception cause = null;

ConfigValue<Path> pathConfig = config.get("path").as(Path.class);
if (pathConfig.isPresent()) {
Path path = pathConfig.get();
List<ConfigSource> result = sourceFromPathMeta(path, fromPath);

if (!result.isEmpty()) {
return result;
}
// else the file was not found, check optional
location = "path " + path.toAbsolutePath();
} else {
ConfigValue<String> classpathConfig = config.get("classpath").as(String.class);
if (classpathConfig.isPresent()) {
String classpath = classpathConfig.get();
List<ConfigSource> sources;

if (profile == null) {
sources = fromClasspath.apply(classpath);
} else {
sources = fromClasspathWithProfile.apply(classpath, profile);
}

if (!sources.isEmpty()) {
return sources;
}
location = "classpath " + classpath;
} else {
ConfigValue<URL> urlConfig = config.get("url").as(URL.class);
if (urlConfig.isPresent()) {
URL url = urlConfig.get();
List<ConfigSource> sources = null;
try {
sources = sourceFromUrlMeta(url, fromUrl);
} catch (ConfigException e) {
cause = e;
}

if (sources != null && !sources.isEmpty()) {
return sources;
}
location = "url " + url;
} else {
throw new ConfigException("MP meta configuration does not contain config source location. Node: " + config
.key());
}
}
}

if (optional) {
return List.of();
}
String message = "Meta configuration could not find non-optional config source on " + location;
if (cause == null) {
throw new ConfigException(message);
} else {
throw new ConfigException(message, cause);
}
}

private List<ConfigSource> sourceFromUrlMeta(URL url, Function<URL, ConfigSource> fromUrl) {
ConfigSource profileSource = null;
ConfigSource mainSource = null;
Exception cause = null;

if (profile != null) {
try {
String profileUrl = toProfileName(url.toString(), profile);
profileSource = fromUrl.apply(new URL(profileUrl));
} catch (Exception e) {
cause = e;
}
}

try {
mainSource = fromUrl.apply(url);
if (cause != null) {
LOGGER.log(Level.FINEST, "Failed to load profile URL resource, succeeded loading main from " + url, cause);
}
} catch (ConfigException e) {
if (cause != null) {
e.addSuppressed(cause);
throw e;
} else {
if (profileSource == null) {
throw e;
} else {
LOGGER.log(Level.FINEST, "Did not find main URL config source from " + url + ", have profile source", e);
}
}
}
return composite(mainSource, profileSource);
}

private List<ConfigSource> sourceFromPathMeta(Path path, Function<Path, ConfigSource> fromPath) {
ConfigSource profileSource = null;
ConfigSource mainSource = null;

if (profile != null) {
Path fileNamePath = path.getFileName();
String fileName = (fileNamePath == null ? "" : fileNamePath.toString());
fileName = toProfileName(fileName, profile);
Path profileSpecific = path.resolveSibling(fileName);
if (Files.exists(profileSpecific) && Files.isRegularFile(profileSpecific)) {
profileSource = fromPath.apply(profileSpecific);
}
}

if (Files.exists(path) && Files.isRegularFile(path)) {
mainSource = fromPath.apply(path);
}

// now handle profile
return composite(mainSource, profileSource);
}

private List<ConfigSource> composite(ConfigSource mainSource, ConfigSource profileSource) {
// now handle profile
if (profileSource == null) {
if (mainSource == null) {
return List.of();
}
return List.of(mainSource);
}
if (mainSource == null) {
return List.of(profileSource);
}

return List.of(MpConfigSources.composite(profileSource, mainSource));
}

@Override
public Config build() {
// the build method MUST NOT modify builder state, as it may be called more than once
Expand Down Expand Up @@ -474,6 +316,10 @@ public Config build() {
Collections.reverse(ordinalSources);
Collections.reverse(ordinalConverters);

if (LOGGER.isLoggable(Level.FINEST)) {
LOGGER.finest("The following config sources are used (ordered): " + ordinalSources);
}

List<ConfigSource> targetSources = new LinkedList<>();
HashMap<Class<?>, Converter<?>> targetConverters = new HashMap<>();

Expand All @@ -484,6 +330,9 @@ public Config build() {

// if we already have a profile configured, we have loaded it and can safely return
if (profile != null) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Built MP config for profile " + profile);
}
return result;
}

Expand All @@ -492,7 +341,12 @@ public Config build() {

// nope, return the result
if (configuredProfile == null) {
LOGGER.fine("Built MP config with no profile");
return result;
} else {
if (LOGGER.isLoggable(Level.FINEST)) {
LOGGER.finest("MP profile configured, rebuilding: " + configuredProfile);
}
}

// yes, update it and re-build with profile information
Expand Down Expand Up @@ -545,6 +399,10 @@ private void addDefaultSources(List<OrdinalSource> targetConfigSources) {
.map(OrdinalSource::new)
.forEach(targetConfigSources::add);
}

if (LOGGER.isLoggable(Level.FINEST)) {
LOGGER.finest("The following default config sources discovered: " + targetConfigSources);
}
}
}

Expand Down Expand Up @@ -627,7 +485,7 @@ private OrdinalSource(ConfigSource source) {
}

private OrdinalSource(ConfigSource source, int ordinal) {
this.ordinal = ordinal;
this.ordinal = findOrdinal(source, ordinal);
this.source = source;
}

Expand Down
Loading

0 comments on commit 84451b4

Please sign in to comment.