From 432477e5f4d49dc26b531fbcbb6fc3879e71a1bf Mon Sep 17 00:00:00 2001 From: Eran Landau Date: Thu, 1 Sep 2016 17:12:36 -0700 Subject: [PATCH 1/3] Basic support for Set, List types in proxy mapping. --- .../archaius/api/PropertyContainer.java | 3 + .../netflix/archaius/ConfigProxyFactory.java | 73 +++++++++++++++++-- .../property/DefaultPropertyContainer.java | 12 +++ .../netflix/archaius/ProxyFactoryTest.java | 65 +++++++++++++++++ 4 files changed, 147 insertions(+), 6 deletions(-) diff --git a/archaius2-api/src/main/java/com/netflix/archaius/api/PropertyContainer.java b/archaius2-api/src/main/java/com/netflix/archaius/api/PropertyContainer.java index 09ee05097..8acd82904 100644 --- a/archaius2-api/src/main/java/com/netflix/archaius/api/PropertyContainer.java +++ b/archaius2-api/src/main/java/com/netflix/archaius/api/PropertyContainer.java @@ -17,6 +17,7 @@ import java.math.BigDecimal; import java.math.BigInteger; +import java.util.function.Function; /** * Container for a single property that can be parse as any type. @@ -85,4 +86,6 @@ public interface PropertyContainer { * should be optimized to call one of the known parsing methods based on type. */ Property asType(Class type, T defaultValue); + + Property asType(Function type, String defaultValue); } \ No newline at end of file diff --git a/archaius2-core/src/main/java/com/netflix/archaius/ConfigProxyFactory.java b/archaius2-core/src/main/java/com/netflix/archaius/ConfigProxyFactory.java index 2c247b087..0e2ceb6eb 100644 --- a/archaius2-core/src/main/java/com/netflix/archaius/ConfigProxyFactory.java +++ b/archaius2-core/src/main/java/com/netflix/archaius/ConfigProxyFactory.java @@ -1,5 +1,7 @@ package com.netflix.archaius; +import com.google.common.base.Preconditions; +import com.google.common.base.Splitter; import com.netflix.archaius.api.Config; import com.netflix.archaius.api.Decoder; import com.netflix.archaius.api.Property; @@ -16,10 +18,17 @@ import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; import java.lang.reflect.Proxy; +import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; +import java.util.SortedSet; +import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.function.Supplier; @@ -206,21 +215,29 @@ T newProxy(final Class type, final String initialPrefix, boolean immutabl verb = ""; } + final Class returnType = m.getReturnType(); + Object defaultValue = null; if (m.getAnnotation(DefaultValue.class) != null) { if (m.isDefault()) { throw new IllegalArgumentException("@DefaultValue cannot be defined on a method with a default implementation for method " + m.getDeclaringClass().getName() + "#" + m.getName()); + } else if ( + Map.class.isAssignableFrom(returnType) || + List.class.isAssignableFrom(returnType) || + Set.class.isAssignableFrom(returnType) ) { + throw new IllegalArgumentException("@DefaultValue cannot be used with collections. Use default method implemenation instead " + + m.getDeclaringClass().getName() + "#" + m.getName()); } + String value = m.getAnnotation(DefaultValue.class).value(); - if (m.getReturnType() == String.class) { + if (returnType == String.class) { defaultValue = config.getString("*", value); } else { - defaultValue = decoder.decode(m.getReturnType(), config.getString("*", value)); + defaultValue = decoder.decode(returnType, config.getString("*", value)); } } - final Class returnType = m.getReturnType(); final PropertyName nameAnnot = m.getAnnotation(PropertyName.class); final String propName = nameAnnot != null && nameAnnot.name() != null ? prefix + nameAnnot.name() @@ -230,14 +247,23 @@ T newProxy(final Class type, final String initialPrefix, boolean immutabl // methods can still return dynamic values if (returnType.equals(Map.class)) { invokers.put(m, createMapProperty(propName, (ParameterizedType)m.getGenericReturnType(), immutable)); + } else if (returnType.equals(Set.class)) { + invokers.put(m, createSetProperty(propName, (ParameterizedType)m.getGenericReturnType(), LinkedHashSet::new)); + } else if (returnType.equals(SortedSet.class)) { + invokers.put(m, createSetProperty(propName, (ParameterizedType)m.getGenericReturnType(), TreeSet::new)); + } else if (returnType.equals(List.class)) { + invokers.put(m, createListProperty(propName, (ParameterizedType)m.getGenericReturnType(), ArrayList::new)); + } else if (returnType.equals(LinkedList.class)) { + invokers.put(m, createListProperty(propName, (ParameterizedType)m.getGenericReturnType(), LinkedList::new)); } else if (returnType.isInterface()) { invokers.put(m, createInterfaceProperty(propName, newProxy(returnType, propName, immutable))); } else if (m.getParameterTypes() != null && m.getParameterTypes().length > 0) { + Preconditions.checkNotNull(nameAnnot, "Missing @PropertyName annotation on " + m.getDeclaringClass().getName() + "#" + m.getName()); invokers.put(m, createParameterizedProperty(returnType, propName, nameAnnot.name(), defaultValue)); } else if (immutable) { - invokers.put(m, createImmutablePropertyWithDefault(m.getReturnType(), propName, defaultValue)); + invokers.put(m, createImmutablePropertyWithDefault(returnType, propName, defaultValue)); } else { - invokers.put(m, createDynamicProperty(m.getReturnType(), propName, defaultValue)); + invokers.put(m, createScalarProperty(returnType, propName, defaultValue)); } } catch (Exception e) { throw new RuntimeException("Error proxying method " + m.getName(), e); @@ -293,6 +319,41 @@ T newProxy(final Class type, final String initialPrefix, boolean immutabl return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class[] { type }, handler); } + protected MethodInvoker createCustomProperty(final Function converter, final String propName) { + final Property prop = propertyFactory + .getProperty(propName) + .asType(converter, null); + return new MethodInvoker() { + @Override + public T invoke(Object obj, Object[] args) { + return prop.get(); + } + + @Override + public String getKey() { + return prop.getKey(); + } + }; + } + + private MethodInvoker createListProperty(String propName, ParameterizedType type, Supplier listSupplier) { + final Class valueType = (Class)type.getActualTypeArguments()[0]; + return createCustomProperty(s -> { + List list = listSupplier.get(); + Splitter.on(",").trimResults().splitToList(s).forEach(v -> list.add(decoder.decode(valueType, v))); + return list; + }, propName); + } + + private MethodInvoker createSetProperty(String propName, ParameterizedType type, Supplier setSupplier) { + final Class valueType = (Class)type.getActualTypeArguments()[0]; + return createCustomProperty(s -> { + Set set = setSupplier.get(); + Splitter.on(",").trimResults().splitToList(s).forEach(v -> set.add(decoder.decode(valueType, v))); + return set; + }, propName); + } + @SuppressWarnings("unchecked") private MethodInvoker createMapProperty(final String propName, final ParameterizedType type, final boolean immutable) { final Class valueType = (Class)type.getActualTypeArguments()[1]; @@ -356,7 +417,7 @@ public T get() { }; } - protected MethodInvoker createDynamicProperty(final Class type, final String propName, final Object defaultValue) { + protected MethodInvoker createScalarProperty(final Class type, final String propName, final Object defaultValue) { final Property prop = propertyFactory .getProperty(propName) .asType(type, (T)defaultValue); diff --git a/archaius2-core/src/main/java/com/netflix/archaius/property/DefaultPropertyContainer.java b/archaius2-core/src/main/java/com/netflix/archaius/property/DefaultPropertyContainer.java index 2a0092ee9..90f3ecdc0 100644 --- a/archaius2-core/src/main/java/com/netflix/archaius/property/DefaultPropertyContainer.java +++ b/archaius2-core/src/main/java/com/netflix/archaius/property/DefaultPropertyContainer.java @@ -22,10 +22,12 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicStampedReference; +import java.util.function.Function; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.base.MoreObjects; import com.netflix.archaius.api.Config; import com.netflix.archaius.api.Property; import com.netflix.archaius.api.PropertyContainer; @@ -397,4 +399,14 @@ protected T resolveCurrent() throws Exception { } } } + + @Override + public Property asType(Function type, String defaultValue) { + return add(new CachedProperty(Type.CUSTOM, null) { + @Override + protected T resolveCurrent() throws Exception { + return type.apply(config.getString(key, defaultValue)); + } + }); + } } diff --git a/archaius2-core/src/test/java/com/netflix/archaius/ProxyFactoryTest.java b/archaius2-core/src/test/java/com/netflix/archaius/ProxyFactoryTest.java index f455af07e..1e6b4763c 100644 --- a/archaius2-core/src/test/java/com/netflix/archaius/ProxyFactoryTest.java +++ b/archaius2-core/src/test/java/com/netflix/archaius/ProxyFactoryTest.java @@ -16,7 +16,13 @@ import org.junit.Assert; import org.junit.Test; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.SortedSet; import javax.annotation.Nullable; @@ -231,4 +237,63 @@ public void testWithLongMap() { sub2 = withArgs.getChildren().get("2"); Assert.assertEquals(789, sub2); } + + public static interface ConfigWithCollections { + List getList(); + + Set getSet(); + + SortedSet getSortedSet(); + + LinkedList getLinkedList(); + } + + @Test + public void testCollections() { + SettableConfig config = new DefaultSettableConfig(); + config.setProperty("list", "5,4,3,2,1"); + config.setProperty("set", "5,4,3,2,1"); + config.setProperty("sortedSet", "5,4,3,2,1"); + config.setProperty("linkedList", "5,4,3,2,1"); + + PropertyFactory factory = DefaultPropertyFactory.from(config); + ConfigProxyFactory proxy = new ConfigProxyFactory(config, config.getDecoder(), factory); + ConfigWithCollections withCollections = proxy.newProxy(ConfigWithCollections.class); + + Assert.assertEquals(Arrays.asList(5,4,3,2,1), new ArrayList<>(withCollections.getLinkedList())); + Assert.assertEquals(Arrays.asList(5,4,3,2,1), new ArrayList<>(withCollections.getList())); + Assert.assertEquals(Arrays.asList(5,4,3,2,1), new ArrayList<>(withCollections.getSet())); + Assert.assertEquals(Arrays.asList(1,2,3,4,5), new ArrayList<>(withCollections.getSortedSet())); + + config.setProperty("list", "6,7,8,9,10"); + Assert.assertEquals(Arrays.asList(6,7,8,9,10), new ArrayList<>(withCollections.getList())); + } + + @Test + public void testCollectionsWithoutValue() { + SettableConfig config = new DefaultSettableConfig(); + + PropertyFactory factory = DefaultPropertyFactory.from(config); + ConfigProxyFactory proxy = new ConfigProxyFactory(config, config.getDecoder(), factory); + ConfigWithCollections withCollections = proxy.newProxy(ConfigWithCollections.class); + + Assert.assertNull(withCollections.getLinkedList()); + Assert.assertNull(withCollections.getList()); + Assert.assertNull(withCollections.getSet()); + Assert.assertNull(withCollections.getSortedSet()); + } + + public static interface ConfigWithCollectionsWithDefaultValueAnnotation { + @DefaultValue("") + LinkedList getLinkedList(); + } + + @Test(expected=RuntimeException.class) + public void testCollectionsWithDefaultValueAnnotation() { + SettableConfig config = new DefaultSettableConfig(); + + PropertyFactory factory = DefaultPropertyFactory.from(config); + ConfigProxyFactory proxy = new ConfigProxyFactory(config, config.getDecoder(), factory); + proxy.newProxy(ConfigWithCollectionsWithDefaultValueAnnotation.class); + } } From 943cf0712dc56afeb516704ee9b3510505946c8c Mon Sep 17 00:00:00 2001 From: Eran Landau Date: Thu, 1 Sep 2016 20:21:25 -0700 Subject: [PATCH 2/3] Remove uses of Guava --- .../netflix/archaius/ConfigProxyFactory.java | 11 ++++++----- .../property/DefaultPropertyContainer.java | 19 +++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/archaius2-core/src/main/java/com/netflix/archaius/ConfigProxyFactory.java b/archaius2-core/src/main/java/com/netflix/archaius/ConfigProxyFactory.java index 0e2ceb6eb..ca8f23e0d 100644 --- a/archaius2-core/src/main/java/com/netflix/archaius/ConfigProxyFactory.java +++ b/archaius2-core/src/main/java/com/netflix/archaius/ConfigProxyFactory.java @@ -1,7 +1,5 @@ package com.netflix.archaius; -import com.google.common.base.Preconditions; -import com.google.common.base.Splitter; import com.netflix.archaius.api.Config; import com.netflix.archaius.api.Decoder; import com.netflix.archaius.api.Property; @@ -19,6 +17,7 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Proxy; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashSet; @@ -258,7 +257,9 @@ T newProxy(final Class type, final String initialPrefix, boolean immutabl } else if (returnType.isInterface()) { invokers.put(m, createInterfaceProperty(propName, newProxy(returnType, propName, immutable))); } else if (m.getParameterTypes() != null && m.getParameterTypes().length > 0) { - Preconditions.checkNotNull(nameAnnot, "Missing @PropertyName annotation on " + m.getDeclaringClass().getName() + "#" + m.getName()); + if (nameAnnot == null) { + throw new IllegalArgumentException("Missing @PropertyName annotation on " + m.getDeclaringClass().getName() + "#" + m.getName()); + } invokers.put(m, createParameterizedProperty(returnType, propName, nameAnnot.name(), defaultValue)); } else if (immutable) { invokers.put(m, createImmutablePropertyWithDefault(returnType, propName, defaultValue)); @@ -340,7 +341,7 @@ private MethodInvoker createListProperty(String propName, ParameterizedType t final Class valueType = (Class)type.getActualTypeArguments()[0]; return createCustomProperty(s -> { List list = listSupplier.get(); - Splitter.on(",").trimResults().splitToList(s).forEach(v -> list.add(decoder.decode(valueType, v))); + Arrays.asList(s.split("\\s*,\\s*")).forEach(v -> list.add(decoder.decode(valueType, v))); return list; }, propName); } @@ -349,7 +350,7 @@ private MethodInvoker createSetProperty(String propName, ParameterizedType ty final Class valueType = (Class)type.getActualTypeArguments()[0]; return createCustomProperty(s -> { Set set = setSupplier.get(); - Splitter.on(",").trimResults().splitToList(s).forEach(v -> set.add(decoder.decode(valueType, v))); + Arrays.asList(s.split("\\s*,\\s*")).forEach(v -> set.add(decoder.decode(valueType, v))); return set; }, propName); } diff --git a/archaius2-core/src/main/java/com/netflix/archaius/property/DefaultPropertyContainer.java b/archaius2-core/src/main/java/com/netflix/archaius/property/DefaultPropertyContainer.java index 90f3ecdc0..3c41a45c0 100644 --- a/archaius2-core/src/main/java/com/netflix/archaius/property/DefaultPropertyContainer.java +++ b/archaius2-core/src/main/java/com/netflix/archaius/property/DefaultPropertyContainer.java @@ -15,6 +15,15 @@ */ package com.netflix.archaius.property; +import com.netflix.archaius.api.Config; +import com.netflix.archaius.api.Property; +import com.netflix.archaius.api.PropertyContainer; +import com.netflix.archaius.api.PropertyListener; +import com.netflix.archaius.property.ListenerManager.ListenerUpdater; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.math.BigDecimal; import java.math.BigInteger; import java.util.concurrent.CopyOnWriteArrayList; @@ -24,16 +33,6 @@ import java.util.concurrent.atomic.AtomicStampedReference; import java.util.function.Function; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.google.common.base.MoreObjects; -import com.netflix.archaius.api.Config; -import com.netflix.archaius.api.Property; -import com.netflix.archaius.api.PropertyContainer; -import com.netflix.archaius.api.PropertyListener; -import com.netflix.archaius.property.ListenerManager.ListenerUpdater; - /** * Implementation of PropertyContainer which reuses the same object for each * type. This implementation assumes that each fast property is mostly accessed From 3d810bb1558071fe1b629c6bf721b835d13d5d9f Mon Sep 17 00:00:00 2001 From: Eran Landau Date: Fri, 2 Sep 2016 13:30:26 -0700 Subject: [PATCH 3/3] Proper support for empty values in collections --- .../netflix/archaius/ConfigProxyFactory.java | 32 +++++----- .../property/DefaultPropertyContainer.java | 5 +- .../netflix/archaius/ProxyFactoryTest.java | 58 +++++++++++++++++-- 3 files changed, 69 insertions(+), 26 deletions(-) diff --git a/archaius2-core/src/main/java/com/netflix/archaius/ConfigProxyFactory.java b/archaius2-core/src/main/java/com/netflix/archaius/ConfigProxyFactory.java index ca8f23e0d..c5ddd1c4e 100644 --- a/archaius2-core/src/main/java/com/netflix/archaius/ConfigProxyFactory.java +++ b/archaius2-core/src/main/java/com/netflix/archaius/ConfigProxyFactory.java @@ -18,6 +18,7 @@ import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashSet; @@ -247,13 +248,13 @@ T newProxy(final Class type, final String initialPrefix, boolean immutabl if (returnType.equals(Map.class)) { invokers.put(m, createMapProperty(propName, (ParameterizedType)m.getGenericReturnType(), immutable)); } else if (returnType.equals(Set.class)) { - invokers.put(m, createSetProperty(propName, (ParameterizedType)m.getGenericReturnType(), LinkedHashSet::new)); + invokers.put(m, createCollectionProperty(propName, (ParameterizedType)m.getGenericReturnType(), LinkedHashSet::new)); } else if (returnType.equals(SortedSet.class)) { - invokers.put(m, createSetProperty(propName, (ParameterizedType)m.getGenericReturnType(), TreeSet::new)); + invokers.put(m, createCollectionProperty(propName, (ParameterizedType)m.getGenericReturnType(), TreeSet::new)); } else if (returnType.equals(List.class)) { - invokers.put(m, createListProperty(propName, (ParameterizedType)m.getGenericReturnType(), ArrayList::new)); + invokers.put(m, createCollectionProperty(propName, (ParameterizedType)m.getGenericReturnType(), ArrayList::new)); } else if (returnType.equals(LinkedList.class)) { - invokers.put(m, createListProperty(propName, (ParameterizedType)m.getGenericReturnType(), LinkedList::new)); + invokers.put(m, createCollectionProperty(propName, (ParameterizedType)m.getGenericReturnType(), LinkedList::new)); } else if (returnType.isInterface()) { invokers.put(m, createInterfaceProperty(propName, newProxy(returnType, propName, immutable))); } else if (m.getParameterTypes() != null && m.getParameterTypes().length > 0) { @@ -323,7 +324,7 @@ T newProxy(final Class type, final String initialPrefix, boolean immutabl protected MethodInvoker createCustomProperty(final Function converter, final String propName) { final Property prop = propertyFactory .getProperty(propName) - .asType(converter, null); + .asType(converter, ""); return new MethodInvoker() { @Override public T invoke(Object obj, Object[] args) { @@ -337,24 +338,21 @@ public String getKey() { }; } - private MethodInvoker createListProperty(String propName, ParameterizedType type, Supplier listSupplier) { + private MethodInvoker createCollectionProperty(String propName, ParameterizedType type, Supplier listSupplier) { final Class valueType = (Class)type.getActualTypeArguments()[0]; return createCustomProperty(s -> { - List list = listSupplier.get(); - Arrays.asList(s.split("\\s*,\\s*")).forEach(v -> list.add(decoder.decode(valueType, v))); + Collection list = listSupplier.get(); + if (!s.isEmpty()) { + Arrays.asList(s.split("\\s*,\\s*")).forEach(v -> { + if (!v.isEmpty() || valueType == String.class) { + list.add(decoder.decode(valueType, v)); + } + }); + } return list; }, propName); } - private MethodInvoker createSetProperty(String propName, ParameterizedType type, Supplier setSupplier) { - final Class valueType = (Class)type.getActualTypeArguments()[0]; - return createCustomProperty(s -> { - Set set = setSupplier.get(); - Arrays.asList(s.split("\\s*,\\s*")).forEach(v -> set.add(decoder.decode(valueType, v))); - return set; - }, propName); - } - @SuppressWarnings("unchecked") private MethodInvoker createMapProperty(final String propName, final ParameterizedType type, final boolean immutable) { final Class valueType = (Class)type.getActualTypeArguments()[1]; diff --git a/archaius2-core/src/main/java/com/netflix/archaius/property/DefaultPropertyContainer.java b/archaius2-core/src/main/java/com/netflix/archaius/property/DefaultPropertyContainer.java index 3c41a45c0..73d3146ea 100644 --- a/archaius2-core/src/main/java/com/netflix/archaius/property/DefaultPropertyContainer.java +++ b/archaius2-core/src/main/java/com/netflix/archaius/property/DefaultPropertyContainer.java @@ -206,9 +206,8 @@ public T get() { T newValue = null; try { newValue = resolveCurrent(); - } - catch (Exception e) { - LOG.warn("Unable to get current version of property '{}'. Error: {}", key, e.getMessage()); + } catch (Exception e) { + LOG.warn("Unable to get current version of property '{}'", key, e); } if (cache.compareAndSet(currentValue, newValue, cacheVersion, latestVersion)) { diff --git a/archaius2-core/src/test/java/com/netflix/archaius/ProxyFactoryTest.java b/archaius2-core/src/test/java/com/netflix/archaius/ProxyFactoryTest.java index 1e6b4763c..fdff03810 100644 --- a/archaius2-core/src/test/java/com/netflix/archaius/ProxyFactoryTest.java +++ b/archaius2-core/src/test/java/com/netflix/archaius/ProxyFactoryTest.java @@ -252,7 +252,7 @@ public static interface ConfigWithCollections { public void testCollections() { SettableConfig config = new DefaultSettableConfig(); config.setProperty("list", "5,4,3,2,1"); - config.setProperty("set", "5,4,3,2,1"); + config.setProperty("set", "1,2,3,5,4"); config.setProperty("sortedSet", "5,4,3,2,1"); config.setProperty("linkedList", "5,4,3,2,1"); @@ -262,13 +262,59 @@ public void testCollections() { Assert.assertEquals(Arrays.asList(5,4,3,2,1), new ArrayList<>(withCollections.getLinkedList())); Assert.assertEquals(Arrays.asList(5,4,3,2,1), new ArrayList<>(withCollections.getList())); - Assert.assertEquals(Arrays.asList(5,4,3,2,1), new ArrayList<>(withCollections.getSet())); + Assert.assertEquals(Arrays.asList(1,2,3,5,4), new ArrayList<>(withCollections.getSet())); Assert.assertEquals(Arrays.asList(1,2,3,4,5), new ArrayList<>(withCollections.getSortedSet())); config.setProperty("list", "6,7,8,9,10"); Assert.assertEquals(Arrays.asList(6,7,8,9,10), new ArrayList<>(withCollections.getList())); } + + @Test + public void emptyNonStringValuesIgnoredInCollections() { + SettableConfig config = new DefaultSettableConfig(); + config.setProperty("list", ",4, ,2,1"); + config.setProperty("set", ",2, ,5,4"); + config.setProperty("sortedSet", ",4, ,2,1"); + config.setProperty("linkedList", ",4, ,2,1"); + + PropertyFactory factory = DefaultPropertyFactory.from(config); + ConfigProxyFactory proxy = new ConfigProxyFactory(config, config.getDecoder(), factory); + ConfigWithCollections withCollections = proxy.newProxy(ConfigWithCollections.class); + + Assert.assertEquals(Arrays.asList(4,2,1), new ArrayList<>(withCollections.getLinkedList())); + Assert.assertEquals(Arrays.asList(4,2,1), new ArrayList<>(withCollections.getList())); + Assert.assertEquals(Arrays.asList(2,5,4), new ArrayList<>(withCollections.getSet())); + Assert.assertEquals(Arrays.asList(1,2,4), new ArrayList<>(withCollections.getSortedSet())); + } + public static interface ConfigWithStringCollections { + List getList(); + + Set getSet(); + + SortedSet getSortedSet(); + + LinkedList getLinkedList(); + } + + @Test + public void emptyStringValuesAreAddedToCollection() { + SettableConfig config = new DefaultSettableConfig(); + config.setProperty("list", ",4, ,2,1"); + config.setProperty("set", ",2, ,5,4"); + config.setProperty("sortedSet", ",4, ,2,1"); + config.setProperty("linkedList", ",4, ,2,1"); + + PropertyFactory factory = DefaultPropertyFactory.from(config); + ConfigProxyFactory proxy = new ConfigProxyFactory(config, config.getDecoder(), factory); + ConfigWithStringCollections withCollections = proxy.newProxy(ConfigWithStringCollections.class); + + Assert.assertEquals(Arrays.asList("", "4","", "2","1"), new ArrayList<>(withCollections.getLinkedList())); + Assert.assertEquals(Arrays.asList("", "4","", "2","1"), new ArrayList<>(withCollections.getList())); + Assert.assertEquals(Arrays.asList("" ,"2","5","4"), new ArrayList<>(withCollections.getSet())); + Assert.assertEquals(Arrays.asList("", "1","2","4"), new ArrayList<>(withCollections.getSortedSet())); + } + @Test public void testCollectionsWithoutValue() { SettableConfig config = new DefaultSettableConfig(); @@ -277,10 +323,10 @@ public void testCollectionsWithoutValue() { ConfigProxyFactory proxy = new ConfigProxyFactory(config, config.getDecoder(), factory); ConfigWithCollections withCollections = proxy.newProxy(ConfigWithCollections.class); - Assert.assertNull(withCollections.getLinkedList()); - Assert.assertNull(withCollections.getList()); - Assert.assertNull(withCollections.getSet()); - Assert.assertNull(withCollections.getSortedSet()); + Assert.assertTrue(withCollections.getLinkedList().isEmpty()); + Assert.assertTrue(withCollections.getList().isEmpty()); + Assert.assertTrue(withCollections.getSet().isEmpty()); + Assert.assertTrue(withCollections.getSortedSet().isEmpty()); } public static interface ConfigWithCollectionsWithDefaultValueAnnotation {