diff --git a/archaius2-api/src/main/java/com/netflix/archaius/api/ArchaiusType.java b/archaius2-api/src/main/java/com/netflix/archaius/api/ArchaiusType.java new file mode 100644 index 00000000..59a19b8d --- /dev/null +++ b/archaius2-api/src/main/java/com/netflix/archaius/api/ArchaiusType.java @@ -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} */ + 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} */ + 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} */ + 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 /*primitive*/, Class /*wrapper*/> PRIMITIVE_WRAPPERS; + static { + Map, 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); + } +} diff --git a/archaius2-api/src/main/java/com/netflix/archaius/api/Config.java b/archaius2-api/src/main/java/com/netflix/archaius/api/Config.java index 7f4ed3e3..53aa3fb5 100644 --- a/archaius2-api/src/main/java/com/netflix/archaius/api/Config.java +++ b/archaius2-api/src/main/java/com/netflix/archaius/api/Config.java @@ -153,8 +153,6 @@ default boolean instrumentationEnabled() { * @return */ T get(Class type, String key); - T get(Class 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. @@ -162,7 +160,27 @@ default boolean instrumentationEnabled() { * @param key * @return */ + T get(Class type, String key, T defaultValue); + + /** + * Get the property from the Decoder. Use this method for polymorphic types such as collections. + *

+ * 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 get(Type type, String key); + /** + * Get the property from the Decoder. Use this method for polymorphic types such as collections. + *

+ * 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 get(Type type, String key, T defaultValue); /** diff --git a/archaius2-api/src/main/java/com/netflix/archaius/api/PropertyRepository.java b/archaius2-api/src/main/java/com/netflix/archaius/api/PropertyRepository.java index acc2cb3d..59c45689 100644 --- a/archaius2-api/src/main/java/com/netflix/archaius/api/PropertyRepository.java +++ b/archaius2-api/src/main/java/com/netflix/archaius/api/PropertyRepository.java @@ -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 { /** @@ -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. + *

+ * 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}) */ Property get(String key, Class 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. + *

+ * 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. + */ Property 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 Property> getList(String key, Class 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 Property> getSet(String key, Class 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 Property> getMap(String key, Class mapKeyType, Class mapValueType) { + return get(key, ArchaiusType.forMapOf(mapKeyType, mapValueType)); + } } diff --git a/archaius2-core/src/test/java/com/netflix/archaius/property/PropertyTest.java b/archaius2-core/src/test/java/com/netflix/archaius/property/PropertyTest.java index dda00db1..4309d72f 100644 --- a/archaius2-core/src/test/java/com/netflix/archaius/property/PropertyTest.java +++ b/archaius2-core/src/test/java/com/netflix/archaius/property/PropertyTest.java @@ -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; @@ -40,19 +47,20 @@ @SuppressWarnings("deprecation") public class PropertyTest { static class MyService { - private Property value; - private Property value2; + private final Property value; + private final Property value2; AtomicInteger setValueCallsCounter; MyService(PropertyFactory config) { setValueCallsCounter = new AtomicInteger(0); value = config.getProperty("foo").asInteger(1); - value.addListener(new MethodInvoker(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(); } @@ -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; @@ -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"); @@ -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 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> intList = factory.getList("foo", int.class); + Assert.assertEquals(Arrays.asList(10, 13, 13, 20), intList.get()); + + // Tests set creation, parsing non-int elements + Property> 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> mapProp = factory.getMap("shmoo", Short.class, Duration.class); + Map 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> 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); @@ -225,6 +264,7 @@ public void unregisterOldCallback() { DefaultPropertyFactory factory = DefaultPropertyFactory.from(config); + //noinspection unchecked PropertyListener listener = Mockito.mock(PropertyListener.class); Property prop = factory.getProperty("foo").asInteger(1); @@ -266,6 +306,7 @@ public void unsubscribeOnChange() { DefaultPropertyFactory factory = DefaultPropertyFactory.from(config); + //noinspection unchecked Consumer consumer = Mockito.mock(Consumer.class); Property prop = factory.getProperty("foo").asInteger(1); @@ -339,6 +380,7 @@ public void chainedPropertyNotification() { config.setProperty("first", 1); DefaultPropertyFactory factory = DefaultPropertyFactory.from(config); + //noinspection unchecked Consumer consumer = Mockito.mock(Consumer.class); Property prop = factory @@ -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 mapper = Mockito.spy(new Function() { @Override public Integer apply(String t) { @@ -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 prop = factory .get("first", String.class) .orElseGet("second")