Skip to content

Commit

Permalink
Add support for java.util.Map to ConfigInstantiator
Browse files Browse the repository at this point in the history
  • Loading branch information
famod committed Feb 14, 2022
1 parent b50232c commit 5781722
Show file tree
Hide file tree
Showing 4 changed files with 263 additions and 23 deletions.
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
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";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ public synchronized Handler[] setBuildTimeHandlers(final Handler[] newHandlers)

public synchronized void buildTimeComplete() {
buildTimeLoggingActivated = false;
clearHandlers();
}

/**
Expand Down Expand Up @@ -238,6 +239,7 @@ public Handler[] clearHandlers() throws SecurityException {
for (Runnable i : logCloseTasks) {
i.run();
}
logCloseTasks.clear();
return super.clearHandlers();
}

Expand Down

0 comments on commit 5781722

Please sign in to comment.