Skip to content

Commit

Permalink
Use config ordinal to determine which config style to use when popula…
Browse files Browse the repository at this point in the history
…ting list, between comma-separated and indexed (#1266)
  • Loading branch information
radcortez authored Dec 6, 2024
1 parent 95881f0 commit 0859318
Show file tree
Hide file tree
Showing 4 changed files with 244 additions and 36 deletions.
5 changes: 3 additions & 2 deletions documentation/src/main/docs/config/indexed-properties.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ The indexed property syntax uses the property name and square brackets with an i

A call to `Config#getValues("my.collection", String.class)`, will automatically create and convert a `List<String>`
that contains the values `dog`, `cat` and `turtle`. A call to `Config#getValues("my.indexed.collection", String.class)`
returns the exact same result. If SmallRye Config finds the same property name in their indexed and unindexed format,
the indexed value has priority.
returns the exact same result. The indexed property format is prioritized when both styles are found in the same
configuration source. When available in multiple sources, the higher ordinal source wins, like any other configuration
lookup.

The indexed property is sorted by its index before being added to the target `Collection`. Any gaps in the indexes do
not resolve to the target `Collection`, which means that the `Collection` result will store all values without empty
Expand Down
122 changes: 97 additions & 25 deletions implementation/src/main/java/io/smallrye/config/SmallRyeConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,25 @@ public <T, C extends Collection<T>> C getValues(String name, Converter<T> conver
// Try legacy / MP comma separated values
return getValue(name, newCollectionConverter(converter, collectionFactory));
}
return getIndexedValues(indexedProperties, converter, collectionFactory);

// Check ordinality of indexed
int indexedOrdinality = Integer.MIN_VALUE;
C collection = collectionFactory.apply(indexedProperties.size());
for (String indexedProperty : indexedProperties) {
ConfigValue indexed = getConfigValue(indexedProperty);
if (indexed.getConfigSourceOrdinal() >= indexedOrdinality) {
indexedOrdinality = indexed.getConfigSourceOrdinal();
}
collection.add(convertValue(indexed, converter));
}

// Use indexed if comma separated empty or higher in ordinality
ConfigValue commaSeparated = getConfigValue(name);
if (commaSeparated == null || indexedOrdinality >= commaSeparated.getConfigSourceOrdinal()) {
return collection;
} else {
return getValue(name, newCollectionConverter(converter, collectionFactory));
}
}

public <T, C extends Collection<T>> C getIndexedValues(String name, Converter<T> converter,
Expand Down Expand Up @@ -384,19 +402,32 @@ <K, V, C extends Collection<V>> Map<K, C> getMapIndexedValues(
@SuppressWarnings("unchecked")
public <T> T getValue(String name, Class<T> propertyType) {
if (propertyType.isArray()) {
ConfigValue configValue = getConfigValue(name);
if (configValue.getValue() != null) {
List<String> indexedProperties = getIndexedProperties(name);
if (indexedProperties.isEmpty()) {
// Try legacy / MP comma separated values
return getValue(name, requireConverter(propertyType));
}

List<?> values = getValues(name, propertyType.getComponentType());
Object array = Array.newInstance(propertyType.getComponentType(), values.size());
for (int i = 0, valuesSize = values.size(); i < valuesSize; i++) {
Array.set(array, i, values.get(i));
// Check ordinality of indexed
int indexedOrdinality = Integer.MIN_VALUE;
Object array = Array.newInstance(propertyType.getComponentType(), indexedProperties.size());
for (int i = 0; i < indexedProperties.size(); i++) {
final String indexedProperty = indexedProperties.get(i);
ConfigValue indexed = getConfigValue(indexedProperty);
if (indexed.getConfigSourceOrdinal() >= indexedOrdinality) {
indexedOrdinality = indexed.getConfigSourceOrdinal();
}
Array.set(array, i, convertValue(indexed, requireConverter(propertyType.getComponentType())));
}
return (T) array;
}

// Use indexed if comma separated empty or higher in ordinality
ConfigValue commaSeparated = getConfigValue(name);
if (commaSeparated == null || indexedOrdinality >= commaSeparated.getConfigSourceOrdinal()) {
return (T) array;
} else {
return convertValue(commaSeparated, requireConverter(propertyType));
}
}
return getValue(name, requireConverter(propertyType));
}

Expand Down Expand Up @@ -502,8 +533,36 @@ public String getRawValue(String name) {
}

@Override
public <T> Optional<T> getOptionalValue(String name, Class<T> aClass) {
return getValue(name, getOptionalConverter(aClass));
@SuppressWarnings("unchecked")
public <T> Optional<T> getOptionalValue(String name, Class<T> propertyType) {
if (propertyType.isArray()) {
List<String> indexedProperties = getIndexedProperties(name);
if (indexedProperties.isEmpty()) {
// Try legacy / MP comma separated values
return getValue(name, getOptionalConverter(propertyType));
}

// Check ordinality of indexed
int indexedOrdinality = Integer.MIN_VALUE;
Object array = Array.newInstance(propertyType.getComponentType(), indexedProperties.size());
for (int i = 0; i < indexedProperties.size(); i++) {
final String indexedProperty = indexedProperties.get(i);
ConfigValue indexed = getConfigValue(indexedProperty);
if (indexed.getConfigSourceOrdinal() >= indexedOrdinality) {
indexedOrdinality = indexed.getConfigSourceOrdinal();
}
Array.set(array, i, convertValue(indexed, requireConverter(propertyType.getComponentType())));
}

// Use indexed if comma separated empty or higher in ordinality
ConfigValue commaSeparated = getConfigValue(name);
if (commaSeparated == null || indexedOrdinality >= commaSeparated.getConfigSourceOrdinal()) {
return (Optional<T>) Optional.of(array);
} else {
return getValue(name, getOptionalConverter(propertyType));
}
}
return getValue(name, getOptionalConverter(propertyType));
}

public <T> Optional<T> getOptionalValue(String name, Converter<T> converter) {
Expand All @@ -521,11 +580,29 @@ public <T, C extends Collection<T>> Optional<C> getOptionalValues(String name, C

public <T, C extends Collection<T>> Optional<C> getOptionalValues(String name, Converter<T> converter,
IntFunction<C> collectionFactory) {
Optional<C> optionalValue = getOptionalValue(name, newCollectionConverter(converter, collectionFactory));
if (optionalValue.isPresent()) {
return optionalValue;
List<String> indexedProperties = getIndexedProperties(name);
if (indexedProperties.isEmpty()) {
// Try legacy / MP comma separated values
return getOptionalValue(name, newCollectionConverter(converter, collectionFactory));
}

// Check ordinality of indexed
int indexedOrdinality = Integer.MIN_VALUE;
C collection = collectionFactory.apply(indexedProperties.size());
for (String indexedProperty : indexedProperties) {
ConfigValue indexed = getConfigValue(indexedProperty);
if (indexed.getValue() != null && indexed.getConfigSourceOrdinal() >= indexedOrdinality) {
indexedOrdinality = indexed.getConfigSourceOrdinal();
}
convertValue(indexed, newOptionalConverter(converter)).ifPresent(collection::add);
}

// Use indexed if comma separated empty or higher in ordinality
ConfigValue commaSeparated = getConfigValue(name);
if (commaSeparated == null || indexedOrdinality >= commaSeparated.getConfigSourceOrdinal()) {
return collection.isEmpty() ? Optional.empty() : Optional.of(collection);
} else {
return getIndexedOptionalValues(name, converter, collectionFactory);
return getOptionalValue(name, newCollectionConverter(converter, collectionFactory));
}
}

Expand All @@ -536,17 +613,13 @@ public <T, C extends Collection<T>> Optional<C> getIndexedOptionalValues(String
return Optional.empty();
}

final C collection = collectionFactory.apply(indexedProperties.size());
C collection = collectionFactory.apply(indexedProperties.size());
for (String indexedProperty : indexedProperties) {
final Optional<T> optionalValue = getOptionalValue(indexedProperty, converter);
Optional<T> optionalValue = getOptionalValue(indexedProperty, converter);
optionalValue.ifPresent(collection::add);
}

if (!collection.isEmpty()) {
return Optional.of(collection);
}

return Optional.empty();
return collection.isEmpty() ? Optional.empty() : Optional.of(collection);
}

/**
Expand Down Expand Up @@ -839,7 +912,7 @@ private static class ConfigSources implements Serializable {
// Init all late sources
List<String> profiles = getProfiles(positiveInterceptors);
List<ConfigSourceWithPriority> sourcesWithPriorities = mapLateSources(sources, negativeInterceptors,
positiveInterceptors, current, profiles, builder, config);
positiveInterceptors, current, profiles, builder);
List<ConfigSource> configSources = getSources(sourcesWithPriorities);

// Rebuild the chain with the late sources and new instances of the interceptors
Expand Down Expand Up @@ -929,8 +1002,7 @@ private static List<ConfigSourceWithPriority> mapLateSources(
final List<ConfigSourceInterceptor> positiveInterceptors,
final ConfigSourceInterceptorContext current,
final List<String> profiles,
final SmallRyeConfigBuilder builder,
final SmallRyeConfig config) {
final SmallRyeConfigBuilder builder) {

ConfigSourceWithPriority.resetLoadPriority();
List<ConfigSourceWithPriority> currentSources = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
Expand All @@ -28,6 +29,7 @@
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;
import java.util.function.Consumer;
import java.util.function.IntFunction;

import org.eclipse.microprofile.config.Config;
Expand Down Expand Up @@ -212,11 +214,7 @@ void getOptionalValuesIndexed() {
@Test
void getOptionalValuesNotIndexed() {
SmallRyeConfig config = new SmallRyeConfigBuilder()
.withSources(config(
"server.environments", "dev,qa",
"server.environments[0]", "dev",
"server.environments[1]", "qa",
"server.environments[2]", "prod"))
.withSources(config("server.environments", "dev,qa"))
.build();

Optional<List<String>> environments = config.getOptionalValues("server.environments", String.class);
Expand All @@ -232,12 +230,18 @@ void getOptionalValuesEmpty() {
.withSources(config("server.environments", ""))
.build();

assertFalse(
config.getIndexedOptionalValues("server.environments", config.requireConverter(String.class), ArrayList::new)
.isPresent());
assertFalse(config.getOptionalValues("server.environments", String.class).isPresent());

SmallRyeConfig configIndexed = new SmallRyeConfigBuilder()
.withSources(config("server.environments[0]", ""))
.build();

assertFalse(configIndexed
.getIndexedOptionalValues("server.environments", config.requireConverter(String.class), ArrayList::new)
.isPresent());
assertFalse(configIndexed.getOptionalValues("server.environments", String.class).isPresent());
}

Expand Down Expand Up @@ -415,7 +419,7 @@ void getValuesMap() {

Converter<String> stringConverter = config.requireConverter(String.class);
Map<String, String> treeMap = config.getValues("my.prop", stringConverter, stringConverter, t -> new TreeMap<>());
assertTrue(treeMap instanceof TreeMap);
assertInstanceOf(TreeMap.class, treeMap);

Optional<Map<String, String>> optionalMap = config.getOptionalValues("my.prop", String.class, String.class);
assertTrue(optionalMap.isPresent());
Expand All @@ -428,7 +432,7 @@ void getValuesMap() {
Optional<Map<String, String>> optionalTreeMap = config.getOptionalValues("my.prop", stringConverter, stringConverter,
t -> new TreeMap<>());
assertTrue(optionalTreeMap.isPresent());
assertTrue(optionalTreeMap.get() instanceof TreeMap);
assertInstanceOf(TreeMap.class, optionalTreeMap.get());

assertTrue(config.getOptionalValues("my.optional", String.class, String.class).isEmpty());
}
Expand Down Expand Up @@ -477,7 +481,7 @@ void getValuesMapList() {
Converter<String> stringConverter = config.requireConverter(String.class);
Map<String, List<String>> treeMap = config.getValues("my.prop", stringConverter, stringConverter, t -> new TreeMap<>(),
ArrayList::new);
assertTrue(treeMap instanceof TreeMap);
assertInstanceOf(TreeMap.class, treeMap);

Optional<Map<String, List<String>>> optionalMap = config.getOptionalValues("my.prop", String.class, String.class,
ArrayList::new);
Expand All @@ -493,7 +497,7 @@ void getValuesMapList() {
Optional<Map<String, List<String>>> optionalTreeMap = config.getOptionalValues("my.prop", stringConverter,
stringConverter, t -> new TreeMap<>(), ArrayList::new);
assertTrue(optionalTreeMap.isPresent());
assertTrue(optionalTreeMap.get() instanceof TreeMap);
assertInstanceOf(TreeMap.class, optionalTreeMap.get());

assertTrue(config.getOptionalValues("my.optional", String.class, String.class, ArrayList::new).isEmpty());
}
Expand Down Expand Up @@ -603,4 +607,102 @@ void propertiesSources(@TempDir Path tempDir) throws Exception {
assertFalse(config.getConfigSources(EnvConfigSource.class).iterator().hasNext());
assertEquals("1234", config.getRawValue("my.prop"));
}

@Test
void overrideIndexed() {
SmallRyeConfig config = new SmallRyeConfigBuilder()
.withSources(config("list[0]", "one", "list[1]", "two"))
.build();

String[] listArray = config.getValue("list", String[].class);
assertEquals(2, listArray.length);
assertEquals("one", listArray[0]);
assertEquals("two", listArray[1]);
List<String> listList = config.getValues("list", String.class);
assertEquals(2, listList.size());
assertEquals("one", listList.get(0));
assertEquals("two", listList.get(1));
Optional<String[]> listOptionalArray = config.getOptionalValue("list", String[].class);
assertTrue(listOptionalArray.isPresent());
listOptionalArray.ifPresent(list -> {
assertEquals(2, list.length);
assertEquals("one", list[0]);
assertEquals("two", list[1]);
});
Optional<List<String>> listOptionalList = config.getOptionalValues("list", String.class);
assertTrue(listOptionalList.isPresent());
listOptionalList.ifPresent(new Consumer<List<String>>() {
@Override
public void accept(final List<String> list) {
assertEquals(2, list.size());
assertEquals("one", list.get(0));
assertEquals("two", list.get(1));
}
});

config = new SmallRyeConfigBuilder()
.withSources(config("list[0]", "one", "list[1]", "two"))
.withSources(new PropertiesConfigSource(Map.of("list", "three,four"), "", 1000))
.build();

listArray = config.getValue("list", String[].class);
assertEquals(2, listArray.length);
assertEquals("three", listArray[0]);
assertEquals("four", listArray[1]);
listList = config.getValues("list", String.class);
assertEquals(2, listList.size());
assertEquals("three", listList.get(0));
assertEquals("four", listList.get(1));
listOptionalArray = config.getOptionalValue("list", String[].class);
assertTrue(listOptionalArray.isPresent());
listOptionalArray.ifPresent(list -> {
assertEquals(2, list.length);
assertEquals("three", list[0]);
assertEquals("four", list[1]);
});
listOptionalList = config.getOptionalValues("list", String.class);
assertTrue(listOptionalList.isPresent());
listOptionalList.ifPresent(new Consumer<List<String>>() {
@Override
public void accept(final List<String> list) {
assertEquals(2, list.size());
assertEquals("three", list.get(0));
assertEquals("four", list.get(1));
}
});
}

@Test
void overrideCommaSeparated() {
SmallRyeConfig config = new SmallRyeConfigBuilder()
.withSources(config("list", "one,two"))
.withSources(new PropertiesConfigSource(Map.of("list[0]", "three", "list[1]", "four"), "", 1000))
.build();

String[] listArray = config.getValue("list", String[].class);
assertEquals(2, listArray.length);
assertEquals("three", listArray[0]);
assertEquals("four", listArray[1]);
List<String> listList = config.getValues("list", String.class);
assertEquals(2, listList.size());
assertEquals("three", listList.get(0));
assertEquals("four", listList.get(1));
Optional<String[]> listOptionalArray = config.getOptionalValue("list", String[].class);
assertTrue(listOptionalArray.isPresent());
listOptionalArray.ifPresent(list -> {
assertEquals(2, list.length);
assertEquals("three", list[0]);
assertEquals("four", list[1]);
});
Optional<List<String>> listOptionalList = config.getOptionalValues("list", String.class);
assertTrue(listOptionalList.isPresent());
listOptionalList.ifPresent(new Consumer<List<String>>() {
@Override
public void accept(final List<String> list) {
assertEquals(2, list.size());
assertEquals("three", list.get(0));
assertEquals("four", list.get(1));
}
});
}
}
Loading

0 comments on commit 0859318

Please sign in to comment.