diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingContext.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingContext.java index cb4bf9ee9..906597950 100644 --- a/implementation/src/main/java/io/smallrye/config/ConfigMappingContext.java +++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingContext.java @@ -323,7 +323,8 @@ public void accept(Function get) { }); } - Set mapKeys = new HashSet<>(); + Map mapKeys = new HashMap<>(); + Map> mapProperties = new HashMap<>(); for (String propertyName : config.getPropertyNames()) { if (propertyName.length() > path.length() + 1 // only consider properties bigger than the map path && (path.isEmpty() || propertyName.charAt(path.length()) == '.') // next char must be a dot (for the key) @@ -337,29 +338,49 @@ public void accept(Function get) { mapProperty.next(); String mapKey = unindexed(mapProperty.getPreviousSegment()); - // Nested property names use the same key for the same element, so track keys and skip if we already handled it - if (mapKeys.contains(mapKey)) { - continue; - } - - mapKeys.add(mapKey); - String mapNext = unindexed(propertyName.substring(0, mapProperty.getPosition())); + mapKeys.computeIfAbsent(mapKey, new Function() { + @Override + public String apply(final String s) { + return unindexed(propertyName.substring(0, mapProperty.getPosition())); + } + }); - nestedCreators.add(new Consumer<>() { + mapProperties.computeIfAbsent(mapKey, new Function>() { @Override - public void accept(Function get) { - // The properties may have been used ih the unnamed key, which cause clashes, so we skip them - if (unnamedKey != null && usedProperties.contains(propertyName)) { + public List apply(final String s) { + return new ArrayList<>(); + } + }); + mapProperties.get(mapKey).add(propertyName); + } + } + + for (Map.Entry mapKey : mapKeys.entrySet()) { + nestedCreators.add(new Consumer<>() { + @Override + public void accept(Function get) { + // The properties may have been used ih the unnamed key, which cause clashes, so we skip them + if (unnamedKey != null) { + boolean allUsed = true; + for (String mapProperty : mapProperties.get(mapKey.getKey())) { + if (!usedProperties.contains(mapProperty)) { + allUsed = false; + break; + } + } + if (allUsed) { return; } + } - V value = (V) get.apply(mapNext); - if (value != null) { - map.put(keyConverter.convert(mapKey), value); - } + // This is the full path plus the map key + V value = (V) get.apply(mapKey.getValue()); + if (value != null) { + map.put(keyConverter.convert(mapKey.getKey()), value); } - }); - } + } + }); + } return map; diff --git a/implementation/src/test/java/io/smallrye/config/ConfigMappingFullTest.java b/implementation/src/test/java/io/smallrye/config/ConfigMappingFullTest.java new file mode 100644 index 000000000..b31ff9f23 --- /dev/null +++ b/implementation/src/test/java/io/smallrye/config/ConfigMappingFullTest.java @@ -0,0 +1,239 @@ +package io.smallrye.config; + +import static io.smallrye.config.KeyValuesConfigSource.config; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.Map; +import java.util.Optional; +import java.util.OptionalInt; + +import org.junit.jupiter.api.Test; + +public class ConfigMappingFullTest { + @Test + void ambiguousUnnamedKeysDefaults() { + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(config( + "datasource.postgresql.jdbc.url", "value", + "datasource.postgresql.password", "value")) + .withMapping(DataSourcesJdbcRuntimeConfig.class) + .withMapping(DataSourcesJdbcBuildTimeConfig.class) + .withMapping(DataSourcesRuntimeConfig.class) + .withMapping(DataSourcesBuildTimeConfig.class) + .build(); + + assertTrue( + config.getConfigMapping(DataSourcesRuntimeConfig.class).dataSources().get("postgresql").password().isPresent()); + + config = new SmallRyeConfigBuilder() + .withSources(config( + "datasource.postgresql.jdbc.url", "value", + "datasource.postgresql.password", "value")) + .withMapping(DataSourcesJdbcBuildTimeConfig.class) + .withMapping(DataSourcesJdbcRuntimeConfig.class) + .withMapping(DataSourcesRuntimeConfig.class) + .withMapping(DataSourcesBuildTimeConfig.class) + .build(); + + assertTrue( + config.getConfigMapping(DataSourcesRuntimeConfig.class).dataSources().get("postgresql").password().isPresent()); + + config = new SmallRyeConfigBuilder() + .withSources(config( + "datasource.postgresql.jdbc.url", "value", + "datasource.postgresql.password", "value")) + .withMapping(DataSourcesJdbcBuildTimeConfig.class) + .withMapping(DataSourcesJdbcRuntimeConfig.class) + .withMapping(DataSourcesBuildTimeConfig.class) + .withMapping(DataSourcesRuntimeConfig.class) + .build(); + + assertTrue( + config.getConfigMapping(DataSourcesRuntimeConfig.class).dataSources().get("postgresql").password().isPresent()); + + config = new SmallRyeConfigBuilder() + .withSources(config( + "datasource.postgresql.jdbc.url", "value", + "datasource.postgresql.password", "value")) + .withMapping(DataSourcesRuntimeConfig.class) + .withMapping(DataSourcesJdbcBuildTimeConfig.class) + .withMapping(DataSourcesJdbcRuntimeConfig.class) + .withMapping(DataSourcesBuildTimeConfig.class) + .build(); + + assertTrue( + config.getConfigMapping(DataSourcesRuntimeConfig.class).dataSources().get("postgresql").password().isPresent()); + } + + @ConfigMapping(prefix = "datasource") + interface DataSourcesRuntimeConfig { + @WithParentName + @WithDefaults + @WithUnnamedKey("") + Map dataSources(); + + interface DataSourceRuntimeConfig { + @WithDefault("true") + boolean active(); + + Optional username(); + + Optional password(); + + Optional credentialsProvider(); + + Optional credentialsProviderName(); + } + } + + @ConfigMapping(prefix = "datasource") + interface DataSourcesJdbcRuntimeConfig { + DataSourceJdbcRuntimeConfig jdbc(); + + @WithParentName + @WithDefaults + Map namedDataSources(); + + interface DataSourceJdbcOuterNamedRuntimeConfig { + DataSourceJdbcRuntimeConfig jdbc(); + } + + interface DataSourceJdbcRuntimeConfig { + Optional url(); + + OptionalInt initialSize(); + + @WithDefault("0") + int minSize(); + + @WithDefault("20") + int maxSize(); + + @WithDefault("2M") + String backgroundValidationInterval(); + + Optional foregroundValidationInterval(); + + @WithDefault("5S") + Optional acquisitionTimeout(); + + Optional leakDetectionInterval(); + + @WithDefault("5M") + String idleRemovalInterval(); + + Optional maxLifetime(); + + @WithDefault("false") + boolean extendedLeakReport(); + + @WithDefault("false") + boolean flushOnClose(); + + @WithDefault("true") + boolean detectStatementLeaks(); + + Optional newConnectionSql(); + + Optional validationQuerySql(); + + @WithDefault("true") + boolean poolingEnabled(); + + Map additionalJdbcProperties(); + + @WithName("telemetry.enabled") + Optional telemetry(); + } + } + + @ConfigMapping(prefix = "datasource") + interface DataSourcesBuildTimeConfig { + @WithParentName + @WithDefaults + @WithUnnamedKey("") + Map dataSources(); + + @WithName("health.enabled") + @WithDefault("true") + boolean healthEnabled(); + + @WithName("metrics.enabled") + @WithDefault("false") + boolean metricsEnabled(); + + @Deprecated + Optional url(); + + @Deprecated + Optional driver(); + + interface DataSourceBuildTimeConfig { + Optional dbKind(); + + Optional dbVersion(); + + DevServicesBuildTimeConfig devservices(); + + @WithDefault("false") + boolean healthExclude(); + + interface DevServicesBuildTimeConfig { + Optional enabled(); + + Optional imageName(); + + Map containerEnv(); + + Map containerProperties(); + + Map properties(); + + OptionalInt port(); + + Optional command(); + + Optional dbName(); + + Optional username(); + + Optional password(); + + Optional initScriptPath(); + + Map volumes(); + + @WithDefault("true") + boolean reuse(); + } + } + } + + @ConfigMapping(prefix = "datasource") + interface DataSourcesJdbcBuildTimeConfig { + @WithParentName + @WithDefaults + @WithUnnamedKey("") + Map dataSources(); + + interface DataSourceJdbcOuterNamedBuildTimeConfig { + DataSourceJdbcBuildTimeConfig jdbc(); + + interface DataSourceJdbcBuildTimeConfig { + @WithParentName + @WithDefault("true") + boolean enabled(); + + Optional driver(); + + Optional enableMetrics(); + + @WithDefault("false") + boolean tracing(); + + @WithDefault("false") + boolean telemetry(); + } + } + } +}