Skip to content

Commit

Permalink
Merge pull request #739 from Netflix/2.x-reading-collections-from-yml
Browse files Browse the repository at this point in the history
Add support for parsing Collections and Maps from the Spring YML format
  • Loading branch information
akang31 authored Jan 15, 2025
2 parents 70da5b1 + 6d4bd30 commit 7ed4e17
Show file tree
Hide file tree
Showing 3 changed files with 187 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,20 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
Expand Down Expand Up @@ -295,9 +303,49 @@ protected <T> T getValue(Type type, String key) {
}
}

private boolean isMap(ParameterizedType type) {
if (type.getRawType() instanceof Class) {
return Map.class.isAssignableFrom((Class<?>) type.getRawType());
}
return false;
}

private boolean isCollection(ParameterizedType type) {
if (type.getRawType() instanceof Class) {
return Collection.class.isAssignableFrom((Class<?>) type.getRawType());
}
return false;

}

@SuppressWarnings("unchecked")
protected <T> T getValueWithDefault(Type type, String key, T defaultValue) {
Object rawProp = getRawProperty(key);
if (rawProp == null && type instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) type;
if (isMap(parameterizedType)) {
Map<?, ?> ret = new LinkedHashMap<>();
String keyAndDelimiter = key + ".";
Type keyType = parameterizedType.getActualTypeArguments()[0];
Type valueType = parameterizedType.getActualTypeArguments()[1];

for (String k : keys()) {
if (k.startsWith(keyAndDelimiter)) {
ret.put(getDecoder().decode(keyType, k.substring(keyAndDelimiter.length())), get(valueType, k));
}
}
return ret.isEmpty() ? defaultValue : (T) Collections.unmodifiableMap(ret);
} else if (isCollection(parameterizedType)) {
Type valueType = parameterizedType.getActualTypeArguments()[0];
List<?> ret = createListForKey(key, valueType);
if (Set.class.isAssignableFrom((Class<?>) parameterizedType.getRawType())) {
Set<?> retSet = new LinkedHashSet<>(ret);
return ret.isEmpty() ? defaultValue : (T) Collections.unmodifiableSet(retSet);
} else {
return ret.isEmpty() ? defaultValue : (T) Collections.unmodifiableList(ret);
}
}
}

// Not found. Return the default.
if (rawProp == null) {
Expand Down Expand Up @@ -365,6 +413,20 @@ protected <T> T getValueWithDefault(Type type, String key, T defaultValue) {
new IllegalArgumentException("Property " + rawProp + " is not convertible to " + type.getTypeName()));
}

private List<?> createListForKey(String key, Type type) {
List<?> vals = new ArrayList<>();
int counter = 0;
while (true) {
String checkKey = String.format("%s[%s]", key, counter++);
if (containsKey(checkKey)) {
vals.add(get(type, checkKey));
} else {
break;
}
}
return vals;
}

@Override
public String resolve(String value) {
return interpolator.create(getLookup()).resolve(value);
Expand Down Expand Up @@ -468,6 +530,12 @@ public Byte getByte(String key, Byte defaultValue) {
@Override
public <T> List<T> getList(String key, Class<T> type) {
Object value = getRawProperty(key);
if (value == null) {
List<?> alternativeListCreation = createListForKey(key, type);
if (!alternativeListCreation.isEmpty()) {
return (List<T>) alternativeListCreation;
}
}
if (value == null) {
return notFound(key);
}
Expand All @@ -490,6 +558,12 @@ public List<?> getList(String key) {
@SuppressWarnings("rawtypes") // Required by legacy API
public List getList(String key, List defaultValue) {
Object value = getRawProperty(key);
if (value == null) {
List<?> alternativeListCreation = createListForKey(key, String.class);
if (!alternativeListCreation.isEmpty()) {
return alternativeListCreation;
}
}
if (value == null) {
return notFound(key, defaultValue);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,37 @@ public void testInvalidInterface() {
containsString("getAnIntWithParam")));
}

@Test
public void testSpringYmlCollections() {
config.setProperty("list[0]", "1");
config.setProperty("list[1]", 2);
config.setProperty("list[2]", "3");

config.setProperty("set[0]", "1");
config.setProperty("set[1]", "2");
config.setProperty("set[2]", 3);
config.setProperty("set[3]", "3");

config.setProperty("map.key1", "1");
config.setProperty("map.key2", 2);
config.setProperty("map.key3", "3");

ConfigWithSpringCollections configWithSpringCollections = proxyFactory.newProxy(ConfigWithSpringCollections.class);
assertEquals(Arrays.asList("1", "2", "3"), configWithSpringCollections.getList());

Set<Integer> set = configWithSpringCollections.getSet();
assertEquals(3, set.size());
assertTrue(set.contains(1));
assertTrue(set.contains(2));
assertTrue(set.contains(3));

Map<String, Integer> map = configWithSpringCollections.getMap();
assertEquals(3, map.size());
assertEquals(1, map.get("key1"));
assertEquals(2, map.get("key2"));
assertEquals(3, map.get("key3"));
}


//////////////////////////////////////////////////////////////////
/// Test Interfaces
Expand Down Expand Up @@ -666,4 +697,10 @@ public interface ConfigWithBadSettings {
// A parametrized method requires a @PropertyName annotation
int getAnIntWithParam(String param);
}

public interface ConfigWithSpringCollections {
List<String> getList();
Set<Integer> getSet();
Map<String, Integer> getMap();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;

import com.netflix.archaius.api.ArchaiusType;
import com.netflix.archaius.api.Config;
import com.netflix.archaius.api.ConfigListener;
import com.netflix.archaius.exceptions.ParseException;
Expand All @@ -35,6 +38,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

Expand All @@ -54,6 +58,23 @@ public class AbstractConfigTest {
entries.put("stringList", "a,b,c");
entries.put("uriList", "http://example.com,http://example.org");
entries.put("underlyingList", Arrays.asList("a", "b", "c"));
entries.put("springYmlList[0]", "1");
entries.put("springYmlList[1]", "2");
entries.put("springYmlList[2]", "3");
entries.put("springYmlIntList[0]", 1);
entries.put("springYmlIntList[1]", 2);
entries.put("springYmlIntList[2]", 3);
// Repeated entry to distinguish set and list
entries.put("springYmlList[3]", "3");
entries.put("springYmlMap.key1", "1");
entries.put("springYmlMap.key2", "2");
entries.put("springYmlMap.key3", "3");
entries.put("springYmlWithSomeInvalidList[0]", "abc,def");
entries.put("springYmlWithSomeInvalidList[1]", "abc");
entries.put("springYmlWithSomeInvalidList[2]", "a=b");
entries.put("springYmlWithSomeInvalidMap.key1", "a=b");
entries.put("springYmlWithSomeInvalidMap.key2", "c");
entries.put("springYmlWithSomeInvalidMap.key3", "d,e");
}

@Override
Expand Down Expand Up @@ -213,4 +234,59 @@ public void testListeners() {
verify(listener).onError(mockError, mockChildConfig);
}
}

@Test
public void testSpringYml() {
// Working cases for set, list, and map
Set<Integer> set =
config.get(ArchaiusType.forSetOf(Integer.class), "springYmlList", Collections.singleton(1));
assertEquals(set.size(), 3);
assertTrue(set.contains(1));
assertTrue(set.contains(2));
assertTrue(set.contains(3));

List<Integer> list =
config.get(ArchaiusType.forListOf(Integer.class), "springYmlList", Arrays.asList(1));
assertEquals(Arrays.asList(1, 2, 3, 3), list);

List<Integer> intList =
config.get(ArchaiusType.forListOf(Integer.class), "springYmlIntList", Arrays.asList(1));
assertEquals(Arrays.asList(1, 2, 3), intList);

Map<String, Integer> map =
config.get(ArchaiusType.forMapOf(String.class, Integer.class),
"springYmlMap", Collections.emptyMap());
assertEquals(map.size(), 3);
assertEquals(1, map.get("key1"));
assertEquals(2, map.get("key2"));
assertEquals(3, map.get("key3"));

// Not a proper list, so we have the default value returned
List<Integer> invalidList =
config.get(ArchaiusType.forListOf(Integer.class), "springYmlMap", Arrays.asList(1));
assertEquals(invalidList, Arrays.asList(1));

// Not a proper set, so we have the default value returned
Set<Integer> invalidSet =
config.get(ArchaiusType.forSetOf(Integer.class), "springYmlMap", Collections.singleton(1));
assertEquals(invalidSet, Collections.singleton(1));

// Not a proper map, so we have the default value returned
Map<String, String> invalidMap =
config.get(
ArchaiusType.forMapOf(String.class, String.class),
"springYmlList",
Collections.singletonMap("default", "default"));
assertEquals(1, invalidMap.size());
assertEquals("default", invalidMap.get("default"));
}

@Test
public void testSpringYamlAsNormalValue() {
// Confirm that values that are intended to be read as a Spring YML Map can still be read normally
// and also do not return values when read at the top level as anything other than a map.
assertEquals("1", config.get(String.class, "springYmlMap.key1"));
assertEquals(2, config.get(Integer.class, "springYmlMap.key2"));
assertEquals(false, config.containsKey("springYmlMap"));
}
}

0 comments on commit 7ed4e17

Please sign in to comment.