Skip to content

Commit 7ed4e17

Browse files
authored
Merge pull request #739 from Netflix/2.x-reading-collections-from-yml
Add support for parsing Collections and Maps from the Spring YML format
2 parents 70da5b1 + 6d4bd30 commit 7ed4e17

File tree

3 files changed

+187
-0
lines changed

3 files changed

+187
-0
lines changed

archaius2-core/src/main/java/com/netflix/archaius/config/AbstractConfig.java

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,20 @@
2828
import org.slf4j.Logger;
2929
import org.slf4j.LoggerFactory;
3030

31+
import java.lang.reflect.ParameterizedType;
3132
import java.lang.reflect.Type;
3233
import java.math.BigDecimal;
3334
import java.math.BigInteger;
35+
import java.util.ArrayList;
36+
import java.util.Collection;
37+
import java.util.Collections;
3438
import java.util.Iterator;
39+
import java.util.LinkedHashMap;
40+
import java.util.LinkedHashSet;
3541
import java.util.List;
42+
import java.util.Map;
3643
import java.util.NoSuchElementException;
44+
import java.util.Set;
3745
import java.util.concurrent.CopyOnWriteArrayList;
3846
import java.util.concurrent.atomic.AtomicInteger;
3947
import java.util.function.BiConsumer;
@@ -295,9 +303,49 @@ protected <T> T getValue(Type type, String key) {
295303
}
296304
}
297305

306+
private boolean isMap(ParameterizedType type) {
307+
if (type.getRawType() instanceof Class) {
308+
return Map.class.isAssignableFrom((Class<?>) type.getRawType());
309+
}
310+
return false;
311+
}
312+
313+
private boolean isCollection(ParameterizedType type) {
314+
if (type.getRawType() instanceof Class) {
315+
return Collection.class.isAssignableFrom((Class<?>) type.getRawType());
316+
}
317+
return false;
318+
319+
}
320+
298321
@SuppressWarnings("unchecked")
299322
protected <T> T getValueWithDefault(Type type, String key, T defaultValue) {
300323
Object rawProp = getRawProperty(key);
324+
if (rawProp == null && type instanceof ParameterizedType) {
325+
ParameterizedType parameterizedType = (ParameterizedType) type;
326+
if (isMap(parameterizedType)) {
327+
Map<?, ?> ret = new LinkedHashMap<>();
328+
String keyAndDelimiter = key + ".";
329+
Type keyType = parameterizedType.getActualTypeArguments()[0];
330+
Type valueType = parameterizedType.getActualTypeArguments()[1];
331+
332+
for (String k : keys()) {
333+
if (k.startsWith(keyAndDelimiter)) {
334+
ret.put(getDecoder().decode(keyType, k.substring(keyAndDelimiter.length())), get(valueType, k));
335+
}
336+
}
337+
return ret.isEmpty() ? defaultValue : (T) Collections.unmodifiableMap(ret);
338+
} else if (isCollection(parameterizedType)) {
339+
Type valueType = parameterizedType.getActualTypeArguments()[0];
340+
List<?> ret = createListForKey(key, valueType);
341+
if (Set.class.isAssignableFrom((Class<?>) parameterizedType.getRawType())) {
342+
Set<?> retSet = new LinkedHashSet<>(ret);
343+
return ret.isEmpty() ? defaultValue : (T) Collections.unmodifiableSet(retSet);
344+
} else {
345+
return ret.isEmpty() ? defaultValue : (T) Collections.unmodifiableList(ret);
346+
}
347+
}
348+
}
301349

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

416+
private List<?> createListForKey(String key, Type type) {
417+
List<?> vals = new ArrayList<>();
418+
int counter = 0;
419+
while (true) {
420+
String checkKey = String.format("%s[%s]", key, counter++);
421+
if (containsKey(checkKey)) {
422+
vals.add(get(type, checkKey));
423+
} else {
424+
break;
425+
}
426+
}
427+
return vals;
428+
}
429+
368430
@Override
369431
public String resolve(String value) {
370432
return interpolator.create(getLookup()).resolve(value);
@@ -468,6 +530,12 @@ public Byte getByte(String key, Byte defaultValue) {
468530
@Override
469531
public <T> List<T> getList(String key, Class<T> type) {
470532
Object value = getRawProperty(key);
533+
if (value == null) {
534+
List<?> alternativeListCreation = createListForKey(key, type);
535+
if (!alternativeListCreation.isEmpty()) {
536+
return (List<T>) alternativeListCreation;
537+
}
538+
}
471539
if (value == null) {
472540
return notFound(key);
473541
}
@@ -490,6 +558,12 @@ public List<?> getList(String key) {
490558
@SuppressWarnings("rawtypes") // Required by legacy API
491559
public List getList(String key, List defaultValue) {
492560
Object value = getRawProperty(key);
561+
if (value == null) {
562+
List<?> alternativeListCreation = createListForKey(key, String.class);
563+
if (!alternativeListCreation.isEmpty()) {
564+
return alternativeListCreation;
565+
}
566+
}
493567
if (value == null) {
494568
return notFound(key, defaultValue);
495569
}

archaius2-core/src/test/java/com/netflix/archaius/ProxyFactoryTest.java

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -438,6 +438,37 @@ public void testInvalidInterface() {
438438
containsString("getAnIntWithParam")));
439439
}
440440

441+
@Test
442+
public void testSpringYmlCollections() {
443+
config.setProperty("list[0]", "1");
444+
config.setProperty("list[1]", 2);
445+
config.setProperty("list[2]", "3");
446+
447+
config.setProperty("set[0]", "1");
448+
config.setProperty("set[1]", "2");
449+
config.setProperty("set[2]", 3);
450+
config.setProperty("set[3]", "3");
451+
452+
config.setProperty("map.key1", "1");
453+
config.setProperty("map.key2", 2);
454+
config.setProperty("map.key3", "3");
455+
456+
ConfigWithSpringCollections configWithSpringCollections = proxyFactory.newProxy(ConfigWithSpringCollections.class);
457+
assertEquals(Arrays.asList("1", "2", "3"), configWithSpringCollections.getList());
458+
459+
Set<Integer> set = configWithSpringCollections.getSet();
460+
assertEquals(3, set.size());
461+
assertTrue(set.contains(1));
462+
assertTrue(set.contains(2));
463+
assertTrue(set.contains(3));
464+
465+
Map<String, Integer> map = configWithSpringCollections.getMap();
466+
assertEquals(3, map.size());
467+
assertEquals(1, map.get("key1"));
468+
assertEquals(2, map.get("key2"));
469+
assertEquals(3, map.get("key3"));
470+
}
471+
441472

442473
//////////////////////////////////////////////////////////////////
443474
/// Test Interfaces
@@ -666,4 +697,10 @@ public interface ConfigWithBadSettings {
666697
// A parametrized method requires a @PropertyName annotation
667698
int getAnIntWithParam(String param);
668699
}
700+
701+
public interface ConfigWithSpringCollections {
702+
List<String> getList();
703+
Set<Integer> getSet();
704+
Map<String, Integer> getMap();
705+
}
669706
}

archaius2-core/src/test/java/com/netflix/archaius/config/AbstractConfigTest.java

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,12 @@
2121
import java.util.Collections;
2222
import java.util.HashMap;
2323
import java.util.Iterator;
24+
import java.util.List;
2425
import java.util.Map;
26+
import java.util.Set;
2527
import java.util.function.BiConsumer;
2628

29+
import com.netflix.archaius.api.ArchaiusType;
2730
import com.netflix.archaius.api.Config;
2831
import com.netflix.archaius.api.ConfigListener;
2932
import com.netflix.archaius.exceptions.ParseException;
@@ -35,6 +38,7 @@
3538
import static org.junit.jupiter.api.Assertions.assertEquals;
3639
import static org.junit.jupiter.api.Assertions.assertFalse;
3740
import static org.junit.jupiter.api.Assertions.assertThrows;
41+
import static org.junit.jupiter.api.Assertions.assertTrue;
3842
import static org.mockito.Mockito.mock;
3943
import static org.mockito.Mockito.verify;
4044

@@ -54,6 +58,23 @@ public class AbstractConfigTest {
5458
entries.put("stringList", "a,b,c");
5559
entries.put("uriList", "http://example.com,http://example.org");
5660
entries.put("underlyingList", Arrays.asList("a", "b", "c"));
61+
entries.put("springYmlList[0]", "1");
62+
entries.put("springYmlList[1]", "2");
63+
entries.put("springYmlList[2]", "3");
64+
entries.put("springYmlIntList[0]", 1);
65+
entries.put("springYmlIntList[1]", 2);
66+
entries.put("springYmlIntList[2]", 3);
67+
// Repeated entry to distinguish set and list
68+
entries.put("springYmlList[3]", "3");
69+
entries.put("springYmlMap.key1", "1");
70+
entries.put("springYmlMap.key2", "2");
71+
entries.put("springYmlMap.key3", "3");
72+
entries.put("springYmlWithSomeInvalidList[0]", "abc,def");
73+
entries.put("springYmlWithSomeInvalidList[1]", "abc");
74+
entries.put("springYmlWithSomeInvalidList[2]", "a=b");
75+
entries.put("springYmlWithSomeInvalidMap.key1", "a=b");
76+
entries.put("springYmlWithSomeInvalidMap.key2", "c");
77+
entries.put("springYmlWithSomeInvalidMap.key3", "d,e");
5778
}
5879

5980
@Override
@@ -213,4 +234,59 @@ public void testListeners() {
213234
verify(listener).onError(mockError, mockChildConfig);
214235
}
215236
}
237+
238+
@Test
239+
public void testSpringYml() {
240+
// Working cases for set, list, and map
241+
Set<Integer> set =
242+
config.get(ArchaiusType.forSetOf(Integer.class), "springYmlList", Collections.singleton(1));
243+
assertEquals(set.size(), 3);
244+
assertTrue(set.contains(1));
245+
assertTrue(set.contains(2));
246+
assertTrue(set.contains(3));
247+
248+
List<Integer> list =
249+
config.get(ArchaiusType.forListOf(Integer.class), "springYmlList", Arrays.asList(1));
250+
assertEquals(Arrays.asList(1, 2, 3, 3), list);
251+
252+
List<Integer> intList =
253+
config.get(ArchaiusType.forListOf(Integer.class), "springYmlIntList", Arrays.asList(1));
254+
assertEquals(Arrays.asList(1, 2, 3), intList);
255+
256+
Map<String, Integer> map =
257+
config.get(ArchaiusType.forMapOf(String.class, Integer.class),
258+
"springYmlMap", Collections.emptyMap());
259+
assertEquals(map.size(), 3);
260+
assertEquals(1, map.get("key1"));
261+
assertEquals(2, map.get("key2"));
262+
assertEquals(3, map.get("key3"));
263+
264+
// Not a proper list, so we have the default value returned
265+
List<Integer> invalidList =
266+
config.get(ArchaiusType.forListOf(Integer.class), "springYmlMap", Arrays.asList(1));
267+
assertEquals(invalidList, Arrays.asList(1));
268+
269+
// Not a proper set, so we have the default value returned
270+
Set<Integer> invalidSet =
271+
config.get(ArchaiusType.forSetOf(Integer.class), "springYmlMap", Collections.singleton(1));
272+
assertEquals(invalidSet, Collections.singleton(1));
273+
274+
// Not a proper map, so we have the default value returned
275+
Map<String, String> invalidMap =
276+
config.get(
277+
ArchaiusType.forMapOf(String.class, String.class),
278+
"springYmlList",
279+
Collections.singletonMap("default", "default"));
280+
assertEquals(1, invalidMap.size());
281+
assertEquals("default", invalidMap.get("default"));
282+
}
283+
284+
@Test
285+
public void testSpringYamlAsNormalValue() {
286+
// Confirm that values that are intended to be read as a Spring YML Map can still be read normally
287+
// and also do not return values when read at the top level as anything other than a map.
288+
assertEquals("1", config.get(String.class, "springYmlMap.key1"));
289+
assertEquals(2, config.get(Integer.class, "springYmlMap.key2"));
290+
assertEquals(false, config.containsKey("springYmlMap"));
291+
}
216292
}

0 commit comments

Comments
 (0)