Skip to content

Commit

Permalink
Merge pull request #439 from elandau/collections_proxy_support
Browse files Browse the repository at this point in the history
Basic support for Set, List types in proxy mapping
  • Loading branch information
elandau authored Sep 6, 2016
2 parents 352131e + 3d810bb commit 8f88ff2
Show file tree
Hide file tree
Showing 4 changed files with 202 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -85,4 +86,6 @@ public interface PropertyContainer {
* should be optimized to call one of the known parsing methods based on type.
*/
<T> Property<T> asType(Class<T> type, T defaultValue);

<T> Property<T> asType(Function<String, T> type, String defaultValue);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -206,21 +215,29 @@ <T> T newProxy(final Class<T> 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()
Expand All @@ -230,14 +247,25 @@ <T> T newProxy(final Class<T> 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);
Expand Down Expand Up @@ -293,6 +321,38 @@ <T> T newProxy(final Class<T> type, final String initialPrefix, boolean immutabl
return (T) Proxy.newProxyInstance(type.getClassLoader(), new Class[] { type }, handler);
}

protected <T> MethodInvoker<T> createCustomProperty(final Function<String, T> converter, final String propName) {
final Property<T> prop = propertyFactory
.getProperty(propName)
.asType(converter, "");
return new MethodInvoker<T>() {
@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<Collection> 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 <T> MethodInvoker<T> createMapProperty(final String propName, final ParameterizedType type, final boolean immutable) {
final Class<?> valueType = (Class<?>)type.getActualTypeArguments()[1];
Expand Down Expand Up @@ -356,7 +416,7 @@ public T get() {
};
}

protected <T> MethodInvoker<T> createDynamicProperty(final Class<T> type, final String propName, final Object defaultValue) {
protected <T> MethodInvoker<T> createScalarProperty(final Class<T> type, final String propName, final Object defaultValue) {
final Property<T> prop = propertyFactory
.getProperty(propName)
.asType(type, (T)defaultValue);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,23 @@
*/
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;
import java.util.concurrent.TimeUnit;
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
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -397,4 +397,14 @@ protected T resolveCurrent() throws Exception {
}
}
}

@Override
public <T> Property<T> asType(Function<String, T> type, String defaultValue) {
return add(new CachedProperty<T>(Type.CUSTOM, null) {
@Override
protected T resolveCurrent() throws Exception {
return type.apply(config.getString(key, defaultValue));
}
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -231,4 +237,109 @@ public void testWithLongMap() {
sub2 = withArgs.getChildren().get("2");
Assert.assertEquals(789, sub2);
}

public static interface ConfigWithCollections {
List<Integer> getList();

Set<Integer> getSet();

SortedSet<Integer> getSortedSet();

LinkedList<Integer> 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<String> getList();

Set<String> getSet();

SortedSet<String> getSortedSet();

LinkedList<String> 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<Integer> 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);
}
}

0 comments on commit 8f88ff2

Please sign in to comment.