Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add utility methods to simplify usage of collection-valued properties. #687

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.netflix.archaius.api;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

/**
* An implementation of {@link ParameterizedType} that can represent the collection types that Archaius can
* handle with the default property value decoders, plus static utility methods for list, set and map types.
*
* @see PropertyRepository#getList(String, Class)
* @see PropertyRepository#getSet(String, Class)
* @see PropertyRepository#getMap(String, Class, Class)
* @see Config#get(Type, String)
* @see Config#get(Type, String, Object)
*/
public class ArchaiusType implements ParameterizedType {

/** Return a ParametrizedType to represent a {@code List<listValuesType>} */
public static ParameterizedType forListOf(Class<?> listValuesType) {
Class<?> maybeWrappedType = PRIMITIVE_WRAPPERS.getOrDefault(listValuesType, listValuesType);
return new ArchaiusType(List.class, new Class<?>[] { maybeWrappedType });
}

/** Return a ParametrizedType to represent a {@code Set<setValuesType>} */
public static ParameterizedType forSetOf(Class<?> setValuesType) {
Class<?> maybeWrappedType = PRIMITIVE_WRAPPERS.getOrDefault(setValuesType, setValuesType);
return new ArchaiusType(Set.class, new Class<?>[] { maybeWrappedType });
}

/** Return a ParametrizedType to represent a {@code Map<mapKeysType, mapValuesType>} */
public static ParameterizedType forMapOf(Class<?> mapKeysTpe, Class<?> mapValuesType) {
Class<?> maybeWrappedKeyType = PRIMITIVE_WRAPPERS.getOrDefault(mapKeysTpe, mapKeysTpe);
Class<?> maybeWrappedValuesType = PRIMITIVE_WRAPPERS.getOrDefault(mapValuesType, mapValuesType);

return new ArchaiusType(Map.class, new Class<?>[] {maybeWrappedKeyType, maybeWrappedValuesType});
}

private final static Map<Class<?> /*primitive*/, Class<?> /*wrapper*/> PRIMITIVE_WRAPPERS;
static {
Map<Class<?>, Class<?>> wrappers = new HashMap<>();
wrappers.put(Integer.TYPE, Integer.class);
wrappers.put(Long.TYPE, Long.class);
wrappers.put(Double.TYPE, Double.class);
wrappers.put(Float.TYPE, Float.class);
wrappers.put(Boolean.TYPE, Boolean.class);
wrappers.put(Character.TYPE, Character.class);
wrappers.put(Byte.TYPE, Byte.class);
wrappers.put(Short.TYPE, Short.class);
wrappers.put(Void.TYPE, Void.class);

PRIMITIVE_WRAPPERS = Collections.unmodifiableMap(wrappers);
}

private final Class<?> rawType;
private final Class<?>[] typeArguments;

private ArchaiusType(Class<?> rawType, Class<?>[] typeArguments) {
this.rawType = Objects.requireNonNull(rawType);
this.typeArguments = Objects.requireNonNull(typeArguments);
if (rawType.isArray()
|| rawType.isPrimitive()
|| rawType.getTypeParameters().length != typeArguments.length) {
throw new IllegalArgumentException("The provided rawType and arguments don't look like a supported parametrized type");
}
}

@Override
public Type[] getActualTypeArguments() {
return typeArguments;
}

@Override
public Type getRawType() {
return rawType;
}

@Override
public Type getOwnerType() {
return null;
}

@Override
public String toString() {
String typeArgumentNames = Arrays.stream(typeArguments).map(Class::getSimpleName).collect(Collectors.joining(","));
return String.format("ParametrizedType for %s<%s>", rawType.getSimpleName(), typeArgumentNames);
}
}
22 changes: 20 additions & 2 deletions archaius2-api/src/main/java/com/netflix/archaius/api/Config.java
Original file line number Diff line number Diff line change
Expand Up @@ -153,16 +153,34 @@ default boolean instrumentationEnabled() {
* @return
*/
<T> T get(Class<T> type, String key);
<T> T get(Class<T> type, String key, T defaultValue);

/**
* Get the property from the Decoder. All basic data types as well any type
* will a valueOf or String constructor will be supported.
* @param type
* @param key
* @return
*/
<T> T get(Class<T> type, String key, T defaultValue);

/**
* Get the property from the Decoder. Use this method for polymorphic types such as collections.
* <p>
* Use the utility methods in {@link ArchaiusType} to get the types for lists, sets and maps.
*
* @see ArchaiusType#forListOf(Class)
* @see ArchaiusType#forSetOf(Class)
* @see ArchaiusType#forMapOf(Class, Class)
*/
<T> T get(Type type, String key);
/**
* Get the property from the Decoder. Use this method for polymorphic types such as collections.
* <p>
* Use the utility methods in {@link ArchaiusType} to get the types for lists, sets and maps.
*
* @see ArchaiusType#forListOf(Class)
* @see ArchaiusType#forSetOf(Class)
* @see ArchaiusType#forMapOf(Class, Class)
*/
<T> T get(Type type, String key, T defaultValue);

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.netflix.archaius.api;

import java.lang.reflect.Type;
import java.util.List;
import java.util.Map;
import java.util.Set;

public interface PropertyRepository {
/**
Expand All @@ -9,12 +12,55 @@ public interface PropertyRepository {
* to a dynamic configuration system and will have its value automatically updated
* whenever the backing configuration is updated. Fallback properties and default values
* may be specified through the {@link Property} API.
* <p>
* This method does not handle polymorphic return types such as collections. Use {@link #get(String, Type)} or one
* of the specialized utility methods in the interface for that case.
*
* @param key Property name
* @param type Type of property value
* @return
* @param type The type for the property value. This *can* be an array type, but not a primitive array
* (ie, you can use {@code Integer[].class} but not {@code int[].class})
*/
<T> Property<T> get(String key, Class<T> type);

/**
* Fetch a property of a specific type. A {@link Property} object is returned regardless of
* whether a key for it exists in the backing configuration. The {@link Property} is attached
* to a dynamic configuration system and will have its value automatically updated
* whenever the backing configuration is updated. Fallback properties and default values
* may be specified through the {@link Property} API.
* <p>
* Use this method to request polymorphic return types such as collections. See the utility methods in
* {@link ArchaiusType} to get types for lists, sets and maps, or call the utility methods in this interface directly.
*
* @see ArchaiusType#forListOf(Class)
* @see ArchaiusType#forSetOf(Class)
* @see ArchaiusType#forMapOf(Class, Class)
* @param key Property name
* @param type Type of property value.
*/
<T> Property<T> get(String key, Type type);

/**
* Fetch a property with a {@link List} value. This is just an utility wrapper around {@link #get(String, Type)}.
* See that method's documentation for more details.
*/
default <V> Property<List<V>> getList(String key, Class<V> listElementType) {
return get(key, ArchaiusType.forListOf(listElementType));
}

/**
* Fetch a property with a {@link Set} value. This is just an utility wrapper around {@link #get(String, Type)}.
* See that method's documentation for more details.
*/
default <V> Property<Set<V>> getSet(String key, Class<V> setElementType) {
return get(key, ArchaiusType.forSetOf(setElementType));
}

/**
* Fetch a property with a {@link Map} value. This is just an utility wrapper around {@link #get(String, Type)}.
* See that method's documentation for more details.
*/
default <K, V> Property<Map<K, V>> getMap(String key, Class<K> mapKeyType, Class<V> mapValueType) {
return get(key, ArchaiusType.forMapOf(mapKeyType, mapValueType));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@

import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
Expand All @@ -40,19 +47,20 @@
@SuppressWarnings("deprecation")
public class PropertyTest {
static class MyService {
private Property<Integer> value;
private Property<Integer> value2;
private final Property<Integer> value;
private final Property<Integer> value2;

AtomicInteger setValueCallsCounter;

MyService(PropertyFactory config) {
setValueCallsCounter = new AtomicInteger(0);
value = config.getProperty("foo").asInteger(1);
value.addListener(new MethodInvoker<Integer>(this, "setValue"));
value.addListener(new MethodInvoker<>(this, "setValue"));
value2 = config.getProperty("foo").asInteger(2);
}

// Called by the config listener.
@SuppressWarnings("unused")
public void setValue(Integer value) {
setValueCallsCounter.incrementAndGet();
}
Expand All @@ -63,8 +71,8 @@ static class CustomType {
static CustomType DEFAULT = new CustomType(1,1);
static CustomType ONE_TWO = new CustomType(1,2);

private int x;
private int y;
private final int x;
private final int y;

CustomType(int x, int y) {
this.x = x;
Expand Down Expand Up @@ -96,7 +104,7 @@ public void test() throws ConfigException {
}

@Test
public void testAllTypes() {
public void testBasicTypes() {
SettableConfig config = new DefaultSettableConfig();
DefaultPropertyFactory factory = DefaultPropertyFactory.from(config);
config.setProperty("foo", "10");
Expand All @@ -121,17 +129,48 @@ public void testAllTypes() {
Assert.assertEquals(BigInteger.TEN, bigIntegerProp.get());
Assert.assertEquals(true, booleanProp.get());
Assert.assertEquals(10, byteProp.get().byteValue());
Assert.assertEquals(10.0, doubleProp.get().doubleValue(), 0.0001);
Assert.assertEquals(10.0f, floatProp.get().floatValue(), 0.0001f);
Assert.assertEquals(10.0, doubleProp.get(), 0.0001);
Assert.assertEquals(10.0f, floatProp.get(), 0.0001f);
Assert.assertEquals(10, intProp.get().intValue());
Assert.assertEquals(10L, longProp.get().longValue());
Assert.assertEquals((short) 10, shortProp.get().shortValue());
Assert.assertEquals("10", stringProp.get());
Assert.assertEquals(CustomType.ONE_TWO, customTypeProp.get());
}

@Test
public void testUpdateDynamicChild() throws ConfigException {
public void testCollectionTypes() {
SettableConfig config = new DefaultSettableConfig();
DefaultPropertyFactory factory = DefaultPropertyFactory.from(config);
config.setProperty("foo", "10,13,13,20");
config.setProperty("shmoo", "1=PT15M,0=PT0S");

// Test array decoding
Property<Byte[]> byteArray = factory.get("foo", Byte[].class);
Assert.assertEquals(new Byte[] {10, 13, 13, 20}, byteArray.get());

// Tests list creation and parsing, decoding of list elements, proper handling if user gives us a primitive type
Property<List<Integer>> intList = factory.getList("foo", int.class);
Assert.assertEquals(Arrays.asList(10, 13, 13, 20), intList.get());

// Tests set creation, parsing non-int elements
Property<Set<Double>> doubleSet = factory.getSet("foo", Double.class);
Assert.assertEquals(new HashSet<>(Arrays.asList(10.0, 13.0, 20.0)), doubleSet.get());

// Test map creation and parsing, keys and values of less-common types
Property<Map<Short, Duration>> mapProp = factory.getMap("shmoo", Short.class, Duration.class);
Map<Short, Duration> expectedMap = new HashMap<>();
expectedMap.put((short) 1, Duration.ofMinutes(15));
expectedMap.put((short) 0, Duration.ZERO);
Assert.assertEquals(expectedMap, mapProp.get());

// Test proper handling of unset properties
Property<Map<CustomType, CustomType>> emptyProperty = factory.getMap("fubar", CustomType.class, CustomType.class);
Assert.assertNull(emptyProperty.get());
}

@Test
public void testUpdateDynamicChild() {
SettableConfig config = new DefaultSettableConfig();
DefaultPropertyFactory factory = DefaultPropertyFactory.from(config);

Expand Down Expand Up @@ -225,6 +264,7 @@ public void unregisterOldCallback() {

DefaultPropertyFactory factory = DefaultPropertyFactory.from(config);

//noinspection unchecked
PropertyListener<Integer> listener = Mockito.mock(PropertyListener.class);

Property<Integer> prop = factory.getProperty("foo").asInteger(1);
Expand Down Expand Up @@ -266,6 +306,7 @@ public void unsubscribeOnChange() {

DefaultPropertyFactory factory = DefaultPropertyFactory.from(config);

//noinspection unchecked
Consumer<Integer> consumer = Mockito.mock(Consumer.class);

Property<Integer> prop = factory.getProperty("foo").asInteger(1);
Expand Down Expand Up @@ -339,6 +380,7 @@ public void chainedPropertyNotification() {
config.setProperty("first", 1);
DefaultPropertyFactory factory = DefaultPropertyFactory.from(config);

//noinspection unchecked
Consumer<Integer> consumer = Mockito.mock(Consumer.class);

Property<Integer> prop = factory
Expand Down Expand Up @@ -377,7 +419,9 @@ public void testCache() {
SettableConfig config = new DefaultSettableConfig();
config.setProperty("foo", "1");
DefaultPropertyFactory factory = DefaultPropertyFactory.from(config);


// This can't be a lambda because then mockito can't subclass it to spy on it :-P
//noinspection Convert2Lambda,Anonymous2MethodRef
Function<String, Integer> mapper = Mockito.spy(new Function<String, Integer>() {
@Override
public Integer apply(String t) {
Expand Down Expand Up @@ -412,7 +456,8 @@ public Integer apply(String t) {
public void mapDiscardsType() {
MapConfig config = MapConfig.builder().build();
DefaultPropertyFactory factory = DefaultPropertyFactory.from(config);


//noinspection unused
Property<Integer> prop = factory
.get("first", String.class)
.orElseGet("second")
Expand Down