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 index 59a19b8d..b2223600 100644 --- a/archaius2-api/src/main/java/com/netflix/archaius/api/ArchaiusType.java +++ b/archaius2-api/src/main/java/com/netflix/archaius/api/ArchaiusType.java @@ -23,19 +23,19 @@ */ public class ArchaiusType implements ParameterizedType { - /** Return a ParametrizedType to represent a {@code List} */ + /** Return a parameterizedType 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} */ + /** Return a parameterizedType 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} */ + /** Return a parameterizedType 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); @@ -68,7 +68,7 @@ private ArchaiusType(Class rawType, Class[] 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"); + throw new IllegalArgumentException("The provided rawType and arguments don't look like a supported parameterized type"); } } @@ -90,6 +90,6 @@ public Type getOwnerType() { @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); + return String.format("parameterizedType for %s<%s>", rawType.getSimpleName(), typeArgumentNames); } } 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 04516926..a35eb16b 100644 --- a/archaius2-core/src/main/java/com/netflix/archaius/ConfigProxyFactory.java +++ b/archaius2-core/src/main/java/com/netflix/archaius/ConfigProxyFactory.java @@ -8,9 +8,9 @@ import com.netflix.archaius.api.annotations.Configuration; import com.netflix.archaius.api.annotations.DefaultValue; import com.netflix.archaius.api.annotations.PropertyName; -import com.netflix.archaius.util.Maps; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.SystemUtils; +import org.apache.commons.lang3.text.StrLookup; import org.apache.commons.lang3.text.StrSubstitutor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,11 +38,11 @@ * Factory for binding a configuration interface to properties in a {@link PropertyFactory} * instance. Getter methods on the interface are mapped by naming convention * by the property name may be overridden using the @PropertyName annotation. - * + *

* For example, *

  * {@code
- * {@literal @}Configuration(prefix="foo")
+ * @Configuration(prefix="foo")
  * interface FooConfiguration {
  *    int getTimeout();     // maps to "foo.timeout"
  *
@@ -55,11 +55,11 @@
  * that the default value type is a string to allow for interpolation.  Alternatively, methods can
  * provide a default method implementation.  Note that {@literal @}DefaultValue cannot be added to a default
  * method as it would introduce ambiguity as to which mechanism wins.
- *
+ * 

* For example, *

  * {@code
- * {@literal @}Configuration(prefix="foo")
+ * @Configuration(prefix="foo")
  * interface FooConfiguration {
  *    @DefaultValue("1000")
  *    int getReadTimeout();     // maps to "foo.timeout"
@@ -78,7 +78,7 @@
  * 
* * To override the prefix in {@literal @}Configuration or provide a prefix when there is no - * @Configuration annotation simply pass in a prefix in the call to newProxy. + * {@literal @}Configuration annotation simply pass in a prefix in the call to newProxy. * *
  * {@code 
@@ -86,14 +86,15 @@
  * }
  * 
* - * By default all properties are dynamic and can therefore change from call to call. To make the + * By default, all properties are dynamic and can therefore change from call to call. To make the * configuration static set the immutable attributes of @Configuration to true. - * + *

* Note that an application should normally have just one instance of ConfigProxyFactory * and PropertyFactory since PropertyFactory caches {@link com.netflix.archaius.api.Property} objects. * - * @see {@literal }@Configuration + * @see Configuration */ +@SuppressWarnings("deprecation") public class ConfigProxyFactory { private static final Logger LOG = LoggerFactory.getLogger(ConfigProxyFactory.class); @@ -103,75 +104,79 @@ public class ConfigProxyFactory { private final Decoder decoder; private final PropertyRepository propertyRepository; private final Config config; - + + + /** + * Build a proxy factory from the provided config, decoder and PropertyFactory. Normal usage from most applications + * is to just set up injection bindings for those 3 objects and let your DI framework find this constructor. + * + * @param config Used to perform string interpolation in values from {@link DefaultValue} annotations. Weird things + * will happen if this is not the same Config that the PropertyFactory exposes! + * @param decoder Used to parse strings from {@link DefaultValue} annotations into the proper types. + * @param factory Used to access the config values that are returned by proxies created by this factory. + */ + @SuppressWarnings("DIAnnotationInspectionTool") @Inject public ConfigProxyFactory(Config config, Decoder decoder, PropertyFactory factory) { this.decoder = decoder; this.config = config; this.propertyRepository = factory; } - + + /** + * Build a proxy factory for a given Config. Use this ONLY if you need proxies associated with a different Config + * that your DI framework would normally give you. + *

+ * The constructed factory will use the Config's Decoder and a {@link DefaultPropertyFactory} built from that same + * Config object. + * @see #ConfigProxyFactory(Config, Decoder, PropertyFactory) + */ @Deprecated - public ConfigProxyFactory(Config config, PropertyFactory factory) { - this.decoder = config.getDecoder(); - this.config = config; - this.propertyRepository = factory; + public ConfigProxyFactory(Config config) { + this(config, config.getDecoder(), DefaultPropertyFactory.from(config)); } - + + /** + * Build a proxy factory for a given Config and PropertyFactory. Use ONLY if you need to use a specialized + * PropertyFactory in your proxies. The constructed proxy factory will use the Config's Decoder. + * @see #ConfigProxyFactory(Config, Decoder, PropertyFactory) + */ @Deprecated - public ConfigProxyFactory(Config config) { - this.decoder = config.getDecoder(); - this.config = config; - this.propertyRepository = DefaultPropertyFactory.from(config); + public ConfigProxyFactory(Config config, PropertyFactory factory) { + this(config, config.getDecoder(), factory); } + /** * Create a proxy for the provided interface type for which all getter methods are bound * to a Property. - * - * @param type - * @param config - * @return */ public T newProxy(final Class type) { return newProxy(type, null); } - private String derivePrefix(Configuration annot, String prefix) { - if (prefix == null && annot != null) { - prefix = annot.prefix(); - if (prefix == null) { - prefix = ""; - } - } - if (prefix == null) - return ""; - - if (prefix.endsWith(".") || prefix.isEmpty()) - return prefix; - - return prefix + "."; - } - + /** + * Create a proxy for the provided interface type for which all getter methods are bound + * to a Property. The proxy uses the provided prefix, even if there is a {@link Configuration} annotation in TYPE. + */ public T newProxy(final Class type, final String initialPrefix) { Configuration annot = type.getAnnotation(Configuration.class); - return newProxy(type, initialPrefix, annot == null ? false : annot.immutable()); + return newProxy(type, initialPrefix, annot != null && annot.immutable()); } /** - * Encapsulated the invocation of a single method of the interface - * - * @param + * Encapsulate the invocation of a single method of the interface */ - interface MethodInvoker { + interface PropertyValueGetter { /** * Invoke the method with the provided arguments - * @param args - * @return */ T invoke(Object[] args); } + /** + * Providers of "empty" defaults for the known collection types that we support as proxy method return types. + */ private static final Map> knownCollections = new HashMap<>(); static { @@ -182,131 +187,169 @@ interface MethodInvoker { knownCollections.put(LinkedList.class, (ignored) -> new LinkedList<>()); } - @SuppressWarnings({ "rawtypes", "unchecked" }) + @SuppressWarnings({"unchecked", "rawtypes"}) T newProxy(final Class type, final String initialPrefix, boolean immutable) { Configuration annot = type.getAnnotation(Configuration.class); final String prefix = derivePrefix(annot, initialPrefix); - + + // There's a circular dependency between these maps and the proxy object. They must be created first because the + // proxy's invocation handler needs to keep a reference to them, but the proxy must be created before they get + // filled because we may need to call methods on the interface in order to fill the maps :-| + final Map> invokers = new HashMap<>(); + final Map propertyNames = new HashMap<>(); + + final InvocationHandler handler = new ConfigProxyInvocationHandler<>(type, prefix, invokers, propertyNames); + + final T proxyObject = (T) Proxy.newProxyInstance(type.getClassLoader(), new Class[] { type }, handler); + // Iterate through all declared methods of the class looking for setter methods. // Each setter will be mapped to a Property for the property name: // prefix + lowerCamelCaseDerivedPropertyName - final Map> invokers = new HashMap<>(); - final Map propertyNames = new HashMap<>(); + for (Method method : type.getMethods()) { + MethodInvokerHolder methodInvokerHolder = buildInvokerForMethod(type, prefix, method, proxyObject, immutable); - final InvocationHandler handler = (proxy, method, args) -> { - MethodInvoker invoker = invokers.get(method); - if (invoker != null) { - return invoker.invoke(args); - } - if ("equals".equals(method.getName())) { - return proxy == args[0]; - } - else if ("hashCode".equals(method.getName())) { - return System.identityHashCode(proxy); - } - else if ("toString".equals(method.getName())) { - StringBuilder sb = new StringBuilder(); - sb.append(type.getSimpleName()).append("["); - sb.append(invokers.entrySet().stream().map(entry -> { - StringBuilder sbProperty = new StringBuilder(); - sbProperty.append(propertyNames.get(entry.getKey()).substring(prefix.length())).append("='"); - try { - sbProperty.append(entry.getValue().invoke(null)); - } catch (Exception e) { - sbProperty.append(e.getMessage()); - } - sbProperty.append("'"); - return sbProperty.toString(); - }).collect(Collectors.joining(","))); - sb.append("]"); - return sb.toString(); + propertyNames.put(method, methodInvokerHolder.propertyName); + + if (immutable) { + // Cache the current value of the property and always return that. + // Note that this will fail for parameterized properties! + Object value = methodInvokerHolder.invoker.invoke(new Object[]{}); + invokers.put(method, (args) -> value); } else { - throw new NoSuchMethodError(method.getName() + " not found on interface " + type.getName()); + invokers.put(method, methodInvokerHolder.invoker); } - }; + } + return proxyObject; + } + /** + * Build the actual prefix to use for config values read by a proxy. + * @param annot The (possibly null) annotation from the proxied interface. + * @param prefix A (possibly null) explicit prefix being passed by the user (or by an upper level proxy, + * in the case of nested interfaces). If present, it always overrides the annotation. + * @return A prefix to be prepended to all the config keys read by the methods in the proxy. If not empty, it will + * always end in a period . + */ + private String derivePrefix(Configuration annot, String prefix) { + if (prefix == null && annot != null) { + prefix = annot.prefix(); + if (prefix == null) { + prefix = ""; + } + } + if (prefix == null) + return ""; - final T proxyObject = (T) Proxy.newProxyInstance(type.getClassLoader(), new Class[] { type }, handler); + if (prefix.endsWith(".") || prefix.isEmpty()) + return prefix; - for (Method m : type.getMethods()) { - try { - final MethodInvoker invoker; + return prefix + "."; + } - final String verb; - if (m.getName().startsWith("get")) { - verb = "get"; - } else if (m.getName().startsWith("is")) { - verb = "is"; - } else { - verb = ""; - } - - final Class returnType = m.getReturnType(); - - Function defaultSupplier = knownCollections.getOrDefault(returnType, (ignored) -> 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 (returnType == String.class) { - defaultSupplier = memoize((T) config.resolve(value)); - } else { - defaultSupplier = memoize(decoder.decode(returnType, config.resolve(value))); - } - } + @SuppressWarnings({"unchecked", "rawtypes"}) + private MethodInvokerHolder buildInvokerForMethod(Class type, String prefix, Method m, T proxyObject, boolean immutable) { + try { - if (m.isDefault()) { - defaultSupplier = createDefaultMethodSupplier(m, type, proxyObject); - } + final Class returnType = m.getReturnType(); + final PropertyName nameAnnot = m.getAnnotation(PropertyName.class); + final String propName = getPropertyName(prefix, m, nameAnnot); - final PropertyName nameAnnot = m.getAnnotation(PropertyName.class); - final String propName = nameAnnot != null && nameAnnot.name() != null - ? prefix + nameAnnot.name() - : prefix + Character.toLowerCase(m.getName().charAt(verb.length())) + m.getName().substring(verb.length() + 1); + // A supplier for the value to be returned when the method's associated property is not set + final Function defaultValueSupplier; - propertyNames.put(m, propName); + if (m.getAnnotation(DefaultValue.class) != null) { + defaultValueSupplier = createAnnotatedMethodSupplier(m, returnType, config, decoder); + } else if (m.isDefault()) { + defaultValueSupplier = createDefaultMethodSupplier(m, type, proxyObject); + } else { + // No default specified in proxied interface. Return "empty" for collection types, null for any other type. + defaultValueSupplier = knownCollections.getOrDefault(returnType, (ignored) -> null); + } - if (!knownCollections.containsKey(returnType) && returnType.isInterface()) { - invoker = createInterfaceProperty(propName, newProxy(returnType, propName, immutable)); - } else if (m.getParameterCount() > 0) { - if (nameAnnot == null) { - throw new IllegalArgumentException("Missing @PropertyName annotation on " + m.getDeclaringClass().getName() + "#" + m.getName()); - } - invoker = createParameterizedProperty(returnType, prefix, nameAnnot.name(), defaultSupplier); - } else { - invoker = createScalarProperty(m.getGenericReturnType(), propName, defaultSupplier); + // This object encapsulates the way to get the value for the current property. + final PropertyValueGetter propertyValueGetter; + + if (!knownCollections.containsKey(returnType) && returnType.isInterface()) { + // Our return type is an interface but not a known collection. We treat it as a nested Config proxy + // interface and create a proxy with it, with the current property name as the initial prefix for nesting. + propertyValueGetter = createInterfaceProperty(propName, newProxy(returnType, propName, immutable)); + + } else if (m.getParameterCount() > 0) { + // A parameterized property. Note that this requires a @PropertyName annotation to extract the interpolation positions! + if (nameAnnot == null) { + throw new IllegalArgumentException("Missing @PropertyName annotation on " + m.getDeclaringClass().getName() + "#" + m.getName()); } - - if (immutable) { - Object value = invoker.invoke(new Object[]{}); - invokers.put(m, (args) -> value); + + // A previous version allowed the full name to be specified, even if the prefix was specified. So, for + // backwards compatibility, we allow both including or excluding the prefix for parameterized names. + String propertyNameTemplate; + if (!StringUtils.isBlank(prefix) && !nameAnnot.name().startsWith(prefix)) { + propertyNameTemplate = prefix + nameAnnot.name(); } else { - invokers.put(m, invoker); + propertyNameTemplate = nameAnnot.name(); } - } catch (Exception e) { - throw new RuntimeException("Error proxying method " + m.getName(), e); + + propertyValueGetter = createParameterizedProperty(returnType, propertyNameTemplate, defaultValueSupplier); + + } else { + // Anything else. + propertyValueGetter = createScalarProperty(m.getGenericReturnType(), propName, defaultValueSupplier); } + + return new MethodInvokerHolder(propertyValueGetter, propName); + } catch (Exception e) { + throw new RuntimeException("Error proxying method " + m.getName(), e); } + } - return proxyObject; + /** + * Compute the name of the property that will be returned by this method. + */ + private static String getPropertyName(String prefix, Method m, PropertyName nameAnnot) { + final String verb; + if (m.getName().startsWith("get")) { + verb = "get"; + } else if (m.getName().startsWith("is")) { + verb = "is"; + } else { + verb = ""; + } + return nameAnnot != null && nameAnnot.name() != null + ? prefix + nameAnnot.name() + : prefix + Character.toLowerCase(m.getName().charAt(verb.length())) + m.getName().substring(verb.length() + 1); } + + /** Build a supplier that returns the (interpolated and decoded) value from the method's @DefaultValue annotation */ + private static Function createAnnotatedMethodSupplier(Method m, Class returnType, Config config, Decoder decoder) { + 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 (returnType == String.class) { + //noinspection unchecked + return memoize((T) config.resolve(value)); // The cast is actually a no-op, T == String here! + } else { + return memoize(decoder.decode(returnType, config.resolve(value))); + } + } + + /** Build a supplier that always returns VALUE */ private static Function memoize(T value) { return (ignored) -> value; } + /** A supplier that calls a default method in the proxied interface and returns its output */ private static Function createDefaultMethodSupplier(Method method, Class type, T proxyObject) { final MethodHandle methodHandle; @@ -342,7 +385,7 @@ private static Function createDefaultMethodSupplier(Method meth return (T) methodHandle.invokeWithArguments(args); } else { // This is a handle to a method WITH arguments, being called with none. This happens when toString() - // is trying to build a representation of a proxy that has a parametrized property AND the interface + // is trying to build a representation of a proxy that has a parameterized property AND the interface // provides a default method for it. There's no good default to return here, so we'll just use null return null; } @@ -353,63 +396,58 @@ private static Function createDefaultMethodSupplier(Method meth }; } - protected MethodInvoker createInterfaceProperty(String propName, final T proxy) { + /** A value getter for a nested Config proxy */ + protected PropertyValueGetter createInterfaceProperty(String propName, final T proxy) { LOG.debug("Creating interface property `{}` for type `{}`", propName, proxy.getClass()); return (args) -> proxy; } - protected MethodInvoker createScalarProperty(final Type type, final String propName, Function next) { + /** + * A value getter for a "simple" property. Returns the value set in config for the given propName, + * or calls the defaultValueSupplier if the property is not set. + */ + protected PropertyValueGetter createScalarProperty(final Type type, final String propName, Function defaultValueSupplier) { LOG.debug("Creating scalar property `{}` for type `{}`", propName, type.getClass()); final Property prop = propertyRepository.get(propName, type); return args -> { T value = prop.get(); - return value != null ? value : next.apply(null); + return value != null ? value : defaultValueSupplier.apply(null); }; } - protected MethodInvoker createParameterizedProperty(final Class returnType, final String prefix, final String nameAnnot, Function next) { - LOG.debug("Creating parameterized property `{}` for type `{}`", prefix + nameAnnot, returnType); - return new MethodInvoker() { - @Override - public T invoke(Object[] args) { - if (args == null) { - // Why would args be null if this is a parametrized property? Because toString() abuses its - // access to this internal representation :-/ - // We'll fall back to trying to call the provider for the default value. That works properly if - // it comes from an annotation or the known collections. Our wrapper for default interface methods - // catches this case and just returns a null, which is probably the least bad response. - return next.apply(null); - } - - // A previous version allowed the full name to be specified, even if the prefix was specified. So, for - // backwards compatibility, we allow both including or excluding the prefix for parameterized names. - String propName = nameAnnot; - if (!StringUtils.isBlank(prefix) && !nameAnnot.startsWith(prefix)) { - propName = prefix + nameAnnot; - } + /** + * A value getter for a parameterized property. Takes the arguments passed to the method call and interpolates them + * into the property name from the method's @PropertyName annotation, then returns the value set in config for the + * computed property name. If not set, it forwards the call with the same parameters to the defaultValueSupplier. + */ + protected PropertyValueGetter createParameterizedProperty(final Class returnType, final String propertyNameTemplate, Function defaultValueSupplier) { + LOG.debug("Creating parameterized property `{}` for type `{}`", propertyNameTemplate, returnType); - // Determine the actual property name by replacing with arguments using the argument index - // to the method. For example, - // @PropertyName(name="foo.${1}.${0}") - // String getFooValue(String arg0, Integer arg1) - // - // called as getFooValue("bar", 1) would look for the property 'foo.1.bar' - Map values = Maps.newHashMap(args.length); - for (int i = 0; i < args.length; i++) { - values.put(String.valueOf(i), args[i]); - } - propName = new StrSubstitutor(values, "${", "}", '$').replace(propName); - T result = getPropertyWithDefault(returnType, propName); - if (result == null) { - result = next.apply(args); - } - return result; + return args -> { + if (args == null) { + // Why would args be null if this is a parameterized property? Because toString() abuses its + // access to this internal representation :-/ + // We'll fall back to trying to call the provider for the default value. That works properly if + // it comes from an annotation or the known collections. Our wrapper for default interface methods + // catches this case and just returns a null, which is probably the least bad response. + return defaultValueSupplier.apply(null); } - R getPropertyWithDefault(Class type, String propName) { - return propertyRepository.get(propName, type).get(); + // Determine the actual property name by replacing with arguments using the argument index + // to the method. For example, + // @PropertyName(name="foo.${1}.${0}") + // String getFooValue(String arg0, Integer arg1) + // + // called as getFooValue("bar", 1) would look for the property 'foo.1.bar' + String interpolatedPropertyName = new StrSubstitutor(new ArrayLookup<>(args), "${", "}", '$') + .replace(propertyNameTemplate); + + T result = propertyRepository.get(interpolatedPropertyName, returnType).get(); + if (result == null) { + result = defaultValueSupplier.apply(args); } - }; + return result; + }; } private static void maybeWrapThenRethrow(Throwable t) { @@ -421,4 +459,108 @@ private static void maybeWrapThenRethrow(Throwable t) { } throw new RuntimeException(t); } + + + /** InvocationHandler for config proxies. */ + private static class ConfigProxyInvocationHandler

implements InvocationHandler { + private final Map> invokers; + private final Class

type; + private final String prefix; + private final Map propertyNames; + + public ConfigProxyInvocationHandler(Class

proxiedClass, String prefix, Map> invokers, Map propertyNames) { + this.invokers = invokers; + this.type = proxiedClass; + this.prefix = prefix; + this.propertyNames = propertyNames; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws NoSuchMethodError{ + PropertyValueGetter invoker = invokers.get(method); + if (invoker != null) { + return invoker.invoke(args); + } + + switch (method.getName()) { + case "equals": + return proxy == args[0]; + case "hashCode": + return System.identityHashCode(proxy); + case "toString": + return proxyToString(); + default: + throw new NoSuchMethodError(method.getName() + " not found on interface " + type.getName()); + } + } + + /** + * Create a reasonable string representation of the proxy object: "InterfaceName[propName=currentValue, ...]". + * For the case of parameterized properties, fudges it and just uses "null" as the value. + */ + private String proxyToString() { + String propertyNamesAndValues = invokers.entrySet().stream() + .map(this::toNameAndValue) + .collect(Collectors.joining(",")); + + return String.format("%s[%s]", type.getSimpleName(), propertyNamesAndValues); + } + + /** Maps one (method, valueGetter) entry to a "propertyName=value" string */ + private String toNameAndValue(Map.Entry> entry) { + String propertyName = propertyNames.get(entry.getKey()).substring(prefix.length()); + Object propertyValue; + try { + // This call should fail for parameterized properties, because the PropertyValueGetter has a non-empty + // argument list. Fortunately, the implementation there cooperates with us and returns a null instead :-) + propertyValue = entry.getValue().invoke(null); + } catch (Exception e) { + // Just in case + propertyValue = e.getMessage(); + } + + return String.format("%s='%s'", propertyName, propertyValue); + } + } + + /** + * A holder for the two pieces of information we compute for each method: Its invoker and the property's name. + * This would just be a record in Java 17 :-) + */ + private static class MethodInvokerHolder { + final PropertyValueGetter invoker; + final String propertyName; + + private MethodInvokerHolder(PropertyValueGetter invoker, String propertyName) { + this.invoker = invoker; + this.propertyName = propertyName; + } + } + + /** Implement apache-commons StrLookup by interpreting the key as an index into an array */ + private static class ArrayLookup extends StrLookup { + private final V[] elements; + + private ArrayLookup(V[] elements) { + super(); + this.elements = elements; + } + + @Override + public String lookup(String key) { + if (elements == null || elements.length == 0 || StringUtils.isBlank(key)) { + return null; + } + + try { + int index = Integer.parseInt(key); + if (index < 0 || index >= elements.length || elements[index] == null) { + return null; + } + return elements[index].toString(); + } catch (NumberFormatException e) { + return null; + } + } + } } 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 56fbedde..6ee94d68 100644 --- a/archaius2-core/src/test/java/com/netflix/archaius/ProxyFactoryTest.java +++ b/archaius2-core/src/test/java/com/netflix/archaius/ProxyFactoryTest.java @@ -28,6 +28,7 @@ import com.netflix.archaius.config.DefaultSettableConfig; import com.netflix.archaius.config.EmptyConfig; +@SuppressWarnings("deprecation") public class ProxyFactoryTest { public enum TestEnum { NONE, @@ -46,6 +47,7 @@ public interface ImmutableConfig { String getValueWithoutDefault2(); } + @SuppressWarnings("unused") public interface BaseConfig { @DefaultValue("basedefault") String getStr(); @@ -60,6 +62,7 @@ default long getLongValueWithDefault() { } } + @SuppressWarnings("UnusedReturnValue") public interface RootConfig extends BaseConfig { @DefaultValue("default") @Override @@ -92,7 +95,7 @@ public interface SubConfig { } public static class SubConfigFromString { - private String[] parts; + private final String[] parts; public SubConfigFromString(String value) { this.parts = value.split(":"); @@ -203,11 +206,11 @@ public void testAllPropertiesSet() { a.getRequiredValue(); Assert.fail("should have failed with no value for requiredValue"); } - catch (Exception e) { + catch (Exception expected) { } } - static interface WithArguments { + interface WithArguments { @PropertyName(name="${0}.abc.${1}") @DefaultValue("default") String getProperty(String part0, int part1); @@ -229,7 +232,7 @@ public void testWithArguments() { } @Configuration(prefix = "foo.bar") - static interface WithArgumentsAndPrefix { + interface WithArgumentsAndPrefix { @PropertyName(name="baz.${0}.abc.${1}") @DefaultValue("default") String getPropertyWithoutPrefix(String part0, int part1); @@ -259,10 +262,16 @@ public void testWithArgumentsAndPrefix() { } + @SuppressWarnings("unused") public interface WithArgumentsAndDefaultMethod { @PropertyName(name="${0}.abc.${1}") - default String getProperty(String part0, int part1) { - return "default"; + default String getPropertyWith2Placeholders(String part0, int part1) { + return "defaultFor2"; + } + + @PropertyName(name="${0}.def") + default String getPropertyWith1Placeholder(String part0) { + return "defaultFor1"; } } @@ -271,14 +280,18 @@ public void testWithArgumentsAndDefaultMethod() { SettableConfig config = new DefaultSettableConfig(); config.setProperty("a.abc.1", "value1"); config.setProperty("b.abc.2", "value2"); + config.setProperty("c.def", "value_c"); PropertyFactory factory = DefaultPropertyFactory.from(config); ConfigProxyFactory proxy = new ConfigProxyFactory(config, config.getDecoder(), factory); WithArgumentsAndDefaultMethod withArgsAndDefM = proxy.newProxy(WithArgumentsAndDefaultMethod.class); - Assert.assertEquals("value1", withArgsAndDefM.getProperty("a", 1)); - Assert.assertEquals("value2", withArgsAndDefM.getProperty("b", 2)); - Assert.assertEquals("default", withArgsAndDefM.getProperty("a", 2)); + Assert.assertEquals("value1", withArgsAndDefM.getPropertyWith2Placeholders("a", 1)); + Assert.assertEquals("value2", withArgsAndDefM.getPropertyWith2Placeholders("b", 2)); + Assert.assertEquals("defaultFor2", withArgsAndDefM.getPropertyWith2Placeholders("a", 2)); + + Assert.assertEquals("value_c", withArgsAndDefM.getPropertyWith1Placeholder("c")); + Assert.assertEquals("defaultFor1", withArgsAndDefM.getPropertyWith1Placeholder("q")); } public interface ConfigWithMaps { @@ -398,7 +411,7 @@ public void emptyNonStringValuesIgnoredInCollections() { Assert.assertEquals(Arrays.asList(1,2,4), new ArrayList<>(withCollections.getSortedSet())); } - public static interface ConfigWithStringCollections { + public interface ConfigWithStringCollections { List getList(); Set getSet(); @@ -455,6 +468,7 @@ public void testCollectionsWithoutValue() { Assert.assertTrue(withCollections.getSortedSet().isEmpty()); } + @SuppressWarnings("unused") public interface ConfigWithCollectionsWithDefaultValueAnnotation { @DefaultValue("") LinkedList getLinkedList(); @@ -485,12 +499,13 @@ public void interfaceDefaultCollections() { ConfigProxyFactory proxy = new ConfigProxyFactory(config, config.getDecoder(), factory); ConfigWithDefaultStringCollections withCollections = proxy.newProxy(ConfigWithDefaultStringCollections.class); - Assert.assertEquals(Arrays.asList("default"), new ArrayList<>(withCollections.getList())); - Assert.assertEquals(Arrays.asList("default"), new ArrayList<>(withCollections.getSet())); - Assert.assertEquals(Arrays.asList("default"), new ArrayList<>(withCollections.getSortedSet())); + Assert.assertEquals(Collections.singletonList("default"), new ArrayList<>(withCollections.getList())); + Assert.assertEquals(Collections.singletonList("default"), new ArrayList<>(withCollections.getSet())); + Assert.assertEquals(Collections.singletonList("default"), new ArrayList<>(withCollections.getSortedSet())); } - public static interface FailingError { + @SuppressWarnings("UnusedReturnValue") + public interface FailingError { default String getValue() { throw new IllegalStateException("error"); } } @@ -506,18 +521,22 @@ public void interfaceWithBadDefault() { @Test public void testObjectMethods() { + // These tests just ensure that toString, equals and hashCode have implementations that don't fail. SettableConfig config = new DefaultSettableConfig(); PropertyFactory factory = DefaultPropertyFactory.from(config); ConfigProxyFactory proxy = new ConfigProxyFactory(config, config.getDecoder(), factory); WithArguments withArgs = proxy.newProxy(WithArguments.class); Assert.assertEquals("WithArguments[${0}.abc.${1}='default']", withArgs.toString()); + //noinspection ObviousNullCheck Assert.assertNotNull(withArgs.hashCode()); - Assert.assertTrue(withArgs.equals(withArgs)); + //noinspection EqualsWithItself + Assert.assertEquals(withArgs, withArgs); } @Test public void testObjectMethods_ClassWithArgumentsAndDefaultMethod() { + // These tests just ensure that toString, equals and hashCode have implementations that don't fail. SettableConfig config = new DefaultSettableConfig(); PropertyFactory factory = DefaultPropertyFactory.from(config); ConfigProxyFactory proxy = new ConfigProxyFactory(config, config.getDecoder(), factory); @@ -525,8 +544,11 @@ public void testObjectMethods_ClassWithArgumentsAndDefaultMethod() { // Printing 'null' here is a compromise. The default method in the interface is being called with a bad signature. // There's nothing the proxy could return here that isn't a lie, but at least this is a mostly harmless lie. - Assert.assertEquals("WithArgumentsAndDefaultMethod[${0}.abc.${1}='null']", withArgs.toString()); + // This test depends implicitly on the iteration order for HashMap, which could change on future Java releases. + Assert.assertEquals("WithArgumentsAndDefaultMethod[${0}.def='null',${0}.abc.${1}='null']", withArgs.toString()); + //noinspection ObviousNullCheck Assert.assertNotNull(withArgs.hashCode()); - Assert.assertTrue(withArgs.equals(withArgs)); + //noinspection EqualsWithItself + Assert.assertEquals(withArgs, withArgs); } }