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..c5ddd1c4e 100644 --- a/archaius2-core/src/main/java/com/netflix/archaius/ConfigProxyFactory.java +++ b/archaius2-core/src/main/java/com/netflix/archaius/ConfigProxyFactory.java @@ -16,10 +16,19 @@ import java.lang.reflect.Method; import java.lang.reflect.ParameterizedType; 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; +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,25 @@ 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, createCollectionProperty(propName, (ParameterizedType)m.getGenericReturnType(), LinkedHashSet::new)); + } else if (returnType.equals(SortedSet.class)) { + invokers.put(m, createCollectionProperty(propName, (ParameterizedType)m.getGenericReturnType(), TreeSet::new)); + } else if (returnType.equals(List.class)) { + invokers.put(m, createCollectionProperty(propName, (ParameterizedType)m.getGenericReturnType(), ArrayList::new)); + } else if (returnType.equals(LinkedList.class)) { + 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) { + 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(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 +321,38 @@ 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, ""); + return new MethodInvoker() { + @Override + public T invoke(Object obj, Object[] args) { + return prop.get(); + } + + @Override + public String getKey() { + return prop.getKey(); + } + }; + } + + private MethodInvoker createCollectionProperty(String propName, ParameterizedType type, Supplier listSupplier) { + final Class valueType = (Class)type.getActualTypeArguments()[0]; + return createCustomProperty(s -> { + 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); + } + @SuppressWarnings("unchecked") private MethodInvoker createMapProperty(final String propName, final ParameterizedType type, final boolean immutable) { final Class valueType = (Class)type.getActualTypeArguments()[1]; @@ -356,7 +416,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..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 @@ -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; @@ -22,15 +31,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicStampedReference; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -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 java.util.function.Function; /** * Implementation of PropertyContainer which reuses the same object for each @@ -205,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)) { @@ -397,4 +397,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..fdff03810 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,109 @@ 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", "1,2,3,5,4"); + 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(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(); + + PropertyFactory factory = DefaultPropertyFactory.from(config); + ConfigProxyFactory proxy = new ConfigProxyFactory(config, config.getDecoder(), factory); + ConfigWithCollections withCollections = proxy.newProxy(ConfigWithCollections.class); + + Assert.assertTrue(withCollections.getLinkedList().isEmpty()); + Assert.assertTrue(withCollections.getList().isEmpty()); + Assert.assertTrue(withCollections.getSet().isEmpty()); + Assert.assertTrue(withCollections.getSortedSet().isEmpty()); + } + + 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); + } }