Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for java.util.Map to ConfigInstantiator #23652

Merged
merged 1 commit into from
Feb 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion core/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,14 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-extension-processor</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
<type>pom</type>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>

<!-- Test -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
Expand All @@ -17,6 +16,8 @@
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.microprofile.config.ConfigProvider;
import org.eclipse.microprofile.config.spi.Converter;
Expand All @@ -33,18 +34,16 @@
* has failed and we are attempting to do some form of recovery via hot deployment
* <p>
* TODO: fully implement this as required, at the moment this is mostly to read the HTTP config when startup fails
* or for basic logging setup in non-Quarkus tests
*/
public class ConfigInstantiator {

// certain well-known classname suffixes that we support
private static Set<String> supportedClassNameSuffix;
private static Set<String> SUPPORTED_CLASS_NAME_SUFFIXES = Set.of("Config", "Configuration");

static {
final Set<String> suffixes = new HashSet<>();
suffixes.add("Config");
suffixes.add("Configuration");
supportedClassNameSuffix = Collections.unmodifiableSet(suffixes);
}
private static final String QUARKUS_PROPERTY_PREFIX = "quarkus.";

private static final Pattern SEGMENT_EXTRACTION_PATTERN = Pattern.compile("(\"[^\"]+\"|[^.\"]+).*");

public static <T> T handleObject(Supplier<T> supplier) {
T o = supplier.get();
Expand All @@ -54,20 +53,31 @@ public static <T> T handleObject(Supplier<T> supplier) {

public static void handleObject(Object o) {
final SmallRyeConfig config = (SmallRyeConfig) ConfigProvider.getConfig();
final Class cls = o.getClass();
final String clsNameSuffix = getClassNameSuffix(o);
if (clsNameSuffix == null) {
// unsupported object type
return;
}

final Class<?> cls = o.getClass();
final String name = dashify(cls.getSimpleName().substring(0, cls.getSimpleName().length() - clsNameSuffix.length()));
handleObject("quarkus." + name, o, config);
handleObject(QUARKUS_PROPERTY_PREFIX + name, o, config, gatherQuarkusPropertyNames(config));
}

private static List<String> gatherQuarkusPropertyNames(SmallRyeConfig config) {
var names = new ArrayList<String>(50);
for (String name : config.getPropertyNames()) {
if (name.startsWith(QUARKUS_PROPERTY_PREFIX)) {
names.add(name);
}
}
return names;
}

private static void handleObject(String prefix, Object o, SmallRyeConfig config) {
private static void handleObject(String prefix, Object o, SmallRyeConfig config, List<String> quarkusPropertyNames) {

try {
final Class cls = o.getClass();
final Class<?> cls = o.getClass();
if (!isClassNameSuffixSupported(o)) {
return;
}
Expand All @@ -83,9 +93,7 @@ private static void handleObject(String prefix, Object o, SmallRyeConfig config)
constructor.setAccessible(true);
Object newInstance = constructor.newInstance();
field.set(o, newInstance);
handleObject(prefix + "." + dashify(field.getName()), newInstance, config);
} else if (fieldClass == Map.class) { //TODO: FIXME, this cannot handle Map yet
field.set(o, new HashMap<>());
handleObject(prefix + "." + dashify(field.getName()), newInstance, config, quarkusPropertyNames);
} else {
String name = configItem.name();
if (name.equals(ConfigItem.HYPHENATED_ELEMENT_NAME)) {
Expand All @@ -95,7 +103,11 @@ private static void handleObject(String prefix, Object o, SmallRyeConfig config)
}
String fullName = prefix + "." + name;
final Type genericType = field.getGenericType();
final Converter<?> conv = getConverterFor(genericType);
if (fieldClass == Map.class) {
field.set(o, handleMap(fullName, genericType, config, quarkusPropertyNames));
continue;
}
final Converter<?> conv = getConverterFor(genericType, config);
try {
Optional<?> value = config.getOptionalValue(fullName, conv);
if (value.isPresent()) {
Expand All @@ -114,16 +126,61 @@ private static void handleObject(String prefix, Object o, SmallRyeConfig config)
}
}

private static Converter<?> getConverterFor(Type type) {
private static Map<?, ?> handleMap(String fullName, Type genericType, SmallRyeConfig config,
List<String> quarkusPropertyNames) throws ReflectiveOperationException {
var map = new HashMap<>();
if (typeOfParameter(genericType, 0) != String.class) { // only support String keys
return map;
}
var processedSegments = new HashSet<String>();
// infer the map keys from existing property names
Copy link
Member Author

@famod famod Feb 13, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I'm extracting the segments/map keys from all present property names.

At this point I have no idea whether or not this covers env var flavors (e.g. QUARKUS_FOO).

for (String propertyName : quarkusPropertyNames) {
var fullNameWithDot = fullName + ".";
String withoutPrefix = propertyName.replace(fullNameWithDot, "");
if (withoutPrefix.equals(propertyName)) {
continue;
}
Matcher matcher = SEGMENT_EXTRACTION_PATTERN.matcher(withoutPrefix);
if (!matcher.find()) {
continue; // should not happen, but be lenient
}
var segment = matcher.group(1);
if (!processedSegments.add(segment)) {
continue;
}
var mapKey = segment.replace("\"", "");
var nextFullName = fullNameWithDot + segment;
var mapValueType = typeOfParameter(genericType, 1);
Object mapValue;
if (mapValueType instanceof ParameterizedType
&& ((ParameterizedType) mapValueType).getRawType().equals(Map.class)) {
mapValue = handleMap(nextFullName, mapValueType, config, quarkusPropertyNames);
} else {
Class<?> mapValueClass = mapValueType instanceof Class ? (Class<?>) mapValueType : null;
if (mapValueClass != null && mapValueClass.isAnnotationPresent(ConfigGroup.class)) {
Constructor<?> constructor = mapValueClass.getConstructor();
constructor.setAccessible(true);
mapValue = constructor.newInstance();
handleObject(nextFullName, mapValue, config, quarkusPropertyNames);
} else {
final Converter<?> conv = getConverterFor(mapValueType, config);
mapValue = config.getOptionalValue(nextFullName, conv).orElse(null);
}
}
map.put(mapKey, mapValue);
}
return map;
}

private static Converter<?> getConverterFor(Type type, SmallRyeConfig config) {
// hopefully this is enough
final SmallRyeConfig config = (SmallRyeConfig) ConfigProvider.getConfig();
Class<?> rawType = rawTypeOf(type);
if (Enum.class.isAssignableFrom(rawType)) {
return new HyphenateEnumConverter(rawType);
} else if (rawType == Optional.class) {
return Converters.newOptionalConverter(getConverterFor(typeOfParameter(type, 0)));
return Converters.newOptionalConverter(getConverterFor(typeOfParameter(type, 0), config));
} else if (rawType == List.class) {
return Converters.newCollectionConverter(getConverterFor(typeOfParameter(type, 0)), ArrayList::new);
return Converters.newCollectionConverter(getConverterFor(typeOfParameter(type, 0), config), ArrayList::new);
} else {
return config.requireConverter(rawTypeOf(type));
}
Expand Down Expand Up @@ -174,7 +231,7 @@ private static String getClassNameSuffix(final Object o) {
return null;
}
final String klassName = o.getClass().getName();
for (final String supportedSuffix : supportedClassNameSuffix) {
for (final String supportedSuffix : SUPPORTED_CLASS_NAME_SUFFIXES) {
if (klassName.endsWith(supportedSuffix)) {
return supportedSuffix;
}
Expand All @@ -187,7 +244,7 @@ private static boolean isClassNameSuffixSupported(final Object o) {
return false;
}
final String klassName = o.getClass().getName();
for (final String supportedSuffix : supportedClassNameSuffix) {
for (final String supportedSuffix : SUPPORTED_CLASS_NAME_SUFFIXES) {
if (klassName.endsWith(supportedSuffix)) {
return true;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
package io.quarkus.runtime.configuration;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.Map;
import java.util.Objects;
import java.util.Set;

import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.spi.ConfigProviderResolver;
import org.eclipse.microprofile.config.spi.ConfigSource;
import org.jboss.logmanager.Level;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

import io.quarkus.runtime.annotations.ConfigGroup;
import io.quarkus.runtime.annotations.ConfigItem;
import io.quarkus.runtime.logging.LogConfig;
import io.smallrye.config.SmallRyeConfigBuilder;

/**
* Tests {@link ConfigInstantiator} with a small test config.
*/
public class ConfigInstantiatorTestCase {

private static final Map<String, String> TEST_CONFIG_MAP = Map.of(
"quarkus.log.category.\"foo.bar\".level", "DEBUG",
"quarkus.log.category.baz.level", "TRACE",

"quarkus.map-of-maps.map-of-string-maps.outer1.inner1", "o1i1",
"quarkus.map-of-maps.map-of-string-maps.outer1.inner2", "o1i2",
"quarkus.map-of-maps.map-of-string-maps.\"outer2.key\".inner1", "o2i1",
"quarkus.map-of-maps.map-of-string-maps.\"outer2.key\".\"inner2.key\"", "o2i2",

"quarkus.map-of-maps.map-of-maps.outer1.inner1.value", "o1i1",
"quarkus.map-of-maps.map-of-maps.outer1.inner2.value", "o1i2",
"quarkus.map-of-maps.map-of-maps.\"outer2.key\".inner1.value", "o2i1",
"quarkus.map-of-maps.map-of-maps.\"outer2.key\".\"inner2.key\".value", "o2i2");

private static Config testConfig;
private static Config cfgToRestore;

@BeforeAll
static void registerTestConfig() {
var localTestConfig = new SmallRyeConfigBuilder()
.addDiscoveredConverters()
.withSources(new TestConfigSource())
.build();

var cfgProviderResolver = ConfigProviderResolver.instance();
try {
cfgProviderResolver.registerConfig(localTestConfig, Thread.currentThread().getContextClassLoader());
} catch (IllegalStateException e) { // a config is already registered; remember for later restoration
cfgToRestore = cfgProviderResolver.getConfig();
cfgProviderResolver.releaseConfig(cfgToRestore);
cfgProviderResolver.registerConfig(localTestConfig, Thread.currentThread().getContextClassLoader());
}
testConfig = localTestConfig;
}

@AfterAll
static void releaseTestConfig() {
var cfgProviderResolver = ConfigProviderResolver.instance();
if (testConfig != null) {
cfgProviderResolver.releaseConfig(testConfig);
if (cfgToRestore != null) {
cfgProviderResolver.registerConfig(cfgToRestore, Thread.currentThread().getContextClassLoader());
}
}
}

@Test
public void handleLogConfig() {
LogConfig logConfig = new LogConfig();
ConfigInstantiator.handleObject(logConfig);

assertThat(logConfig.level).isEqualTo(Level.INFO);
assertThat(logConfig.categories).hasSize(2);
// note: category assertions are a bit awkward because most fields and classes are just package visible
// (level.level selects the actual level member of InheritableLevel.ActualLevel)
assertThat(logConfig.categories.get("foo.bar"))
.hasFieldOrPropertyWithValue("level.level", Level.DEBUG);
assertThat(logConfig.categories.get("baz"))
.hasFieldOrPropertyWithValue("level.level", Level.TRACE);
}

@Test
public void handleMapOfMapConfig() {
MapOfMapsConfig mapOfMapsConfig = new MapOfMapsConfig();
ConfigInstantiator.handleObject(mapOfMapsConfig);

assertThat(mapOfMapsConfig.mapOfStringMaps).hasSize(2);
assertThat(mapOfMapsConfig.mapOfStringMaps.get("outer1"))
.isEqualTo(Map.of("inner1", "o1i1", "inner2", "o1i2"));
assertThat(mapOfMapsConfig.mapOfStringMaps.get("outer2.key"))
.isEqualTo(Map.of("inner1", "o2i1", "inner2.key", "o2i2"));

assertThat(mapOfMapsConfig.mapOfMaps).hasSize(2);
assertThat(mapOfMapsConfig.mapOfMaps.get("outer1"))
.isEqualTo(Map.of("inner1", new MapValueConfig("o1i1"), "inner2", new MapValueConfig("o1i2")));
assertThat(mapOfMapsConfig.mapOfMaps.get("outer2.key"))
.isEqualTo(Map.of("inner1", new MapValueConfig("o2i1"), "inner2.key", new MapValueConfig("o2i2")));
}

private static class MapOfMapsConfig {

@ConfigItem
public Map<String, Map<String, String>> mapOfStringMaps;

@ConfigItem
public Map<String, Map<String, MapValueConfig>> mapOfMaps;
}

@ConfigGroup
static class MapValueConfig {

@ConfigItem
public String value;

// value constructor, equals (hashCode) and toString for easy testing:

public MapValueConfig() {
}

MapValueConfig(String value) {
this.value = value;
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
MapValueConfig other = (MapValueConfig) obj;
return Objects.equals(value, other.value);
}

@Override
public int hashCode() {
return Objects.hash(value);
}

@Override
public String toString() {
return String.format("MapValueConfig[%s]", value);
}
}

private static class TestConfigSource implements ConfigSource {

public Map<String, String> getProperties() {
return TEST_CONFIG_MAP;
}

public Set<String> getPropertyNames() {
return TEST_CONFIG_MAP.keySet();
}

public String getValue(final String propertyName) {
return TEST_CONFIG_MAP.get(propertyName);
}

public String getName() {
return "ConfigInstantiatorTestCase config source";
}
}
}
Loading