From 906a0a48ae3ab94146797c175f8aa87645ca5780 Mon Sep 17 00:00:00 2001 From: Patrick Strawderman Date: Tue, 13 Feb 2024 12:50:42 -0800 Subject: [PATCH] Allow specifying custom converters in DefaultDecoder Add a factory method to DefaultDecoder, newCustomDecoder, which allows users to specify custom TypeConverter factories; the existing default converter factories still get installed when the factory method is used, but they are added after the supplied custom factories. --- .../com/netflix/archaius/DefaultDecoder.java | 20 ++++++++++++++ .../netflix/archaius/DefaultDecoderTest.java | 25 ++++++++++++++++- .../netflix/archaius/ProxyFactoryTest.java | 27 +++++-------------- 3 files changed, 51 insertions(+), 21 deletions(-) diff --git a/archaius2-core/src/main/java/com/netflix/archaius/DefaultDecoder.java b/archaius2-core/src/main/java/com/netflix/archaius/DefaultDecoder.java index 7d020e9a..37185d7e 100644 --- a/archaius2-core/src/main/java/com/netflix/archaius/DefaultDecoder.java +++ b/archaius2-core/src/main/java/com/netflix/archaius/DefaultDecoder.java @@ -28,8 +28,11 @@ import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Stream; @@ -43,12 +46,29 @@ public class DefaultDecoder implements Decoder, TypeConverter.Registry { public static final DefaultDecoder INSTANCE = new DefaultDecoder(); private DefaultDecoder() { + this(Collections.emptyList()); + } + + /** + * Create a new {@code DefaultDecoder} with the supplied {@code TypeConverter.Factory} instances installed. + * The default converter factories will still be registered, but will be installed AFTER any custom ones, + * giving callers the opportunity to override the behavior of the default converters. + * + * @param typeConverterFactories the collection of converter factories to use for this decoder + */ + private DefaultDecoder(Collection typeConverterFactories) { + factories.addAll(typeConverterFactories); factories.add(DefaultTypeConverterFactory.INSTANCE); factories.add(DefaultCollectionsTypeConverterFactory.INSTANCE); factories.add(ArrayTypeConverterFactory.INSTANCE); factories.add(EnumTypeConverterFactory.INSTANCE); } + public static DefaultDecoder newCustomDecoder(Collection customTypeConverterFactories) { + Objects.requireNonNull(customTypeConverterFactories, "customTypeConverterFactories == null"); + return customTypeConverterFactories.isEmpty() ? DefaultDecoder.INSTANCE : new DefaultDecoder(customTypeConverterFactories); + } + @Override public T decode(Class type, String encoded) { return decode((Type)type, encoded); diff --git a/archaius2-core/src/test/java/com/netflix/archaius/DefaultDecoderTest.java b/archaius2-core/src/test/java/com/netflix/archaius/DefaultDecoderTest.java index 9c3cfe67..a1c9edb1 100644 --- a/archaius2-core/src/test/java/com/netflix/archaius/DefaultDecoderTest.java +++ b/archaius2-core/src/test/java/com/netflix/archaius/DefaultDecoderTest.java @@ -15,7 +15,6 @@ */ package com.netflix.archaius; - import java.math.BigDecimal; import java.math.BigInteger; import java.net.URI; @@ -29,12 +28,15 @@ import java.time.Period; import java.time.ZonedDateTime; import java.util.BitSet; +import java.util.Collections; import java.util.Currency; import java.util.Date; import java.util.Locale; +import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; +import com.netflix.archaius.api.TypeConverter; import org.apache.commons.codec.DecoderException; import org.apache.commons.codec.binary.Hex; import org.junit.Assert; @@ -97,4 +99,25 @@ public void testTypeConverterRegistry() { class Foo {} Assert.assertFalse(DefaultDecoder.INSTANCE.get(Foo.class).isPresent()); } + + @Test + public void testCustomTypeConverters() { + TypeConverter stringConverter = String::toUpperCase; + TypeConverter longConverter = value -> Long.parseLong(value) * 2; + TypeConverter.Factory factory = (type, registry) -> { + if ((type instanceof Class && ((Class) type).isAssignableFrom(String.class))) { + return Optional.of(stringConverter); + } else if (type.equals(Long.class)) { // override default converter + return Optional.of(longConverter); + } + return Optional.empty(); + }; + DefaultDecoder decoder = DefaultDecoder.newCustomDecoder(Collections.singletonList(factory)); + Assert.assertEquals("FOO", decoder.decode(CharSequence.class, "foo")); + Assert.assertEquals("FOO", decoder.decode(String.class, "foo")); + // default is overridden + Assert.assertEquals(6L, decoder.decode(Long.class, "3").longValue()); + // default converter is used + Assert.assertEquals(3, decoder.decode(Integer.class, "3").intValue()); + } } 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 92069989..d7ebeeb2 100644 --- a/archaius2-core/src/test/java/com/netflix/archaius/ProxyFactoryTest.java +++ b/archaius2-core/src/test/java/com/netflix/archaius/ProxyFactoryTest.java @@ -593,31 +593,18 @@ interface CustomObject { @Test public void testNestedInterfaceWithCustomDecoder() { TypeConverter customObjectTypeConverter = value -> value::toUpperCase; - - final class CustomDecoder implements Decoder, TypeConverter.Registry { - @Override - public T decode(Class type, String encoded) { - if (type.equals(ConfigWithNestedInterface.CustomObject.class)) { - @SuppressWarnings("unchecked") - T converted = (T) customObjectTypeConverter.convert(encoded); - return converted; - } - return DefaultDecoder.INSTANCE.decode(type, encoded); - } - - @Override - public Optional> get(Type type) { - if (type.equals(ConfigWithNestedInterface.CustomObject.class)) { - return Optional.of(customObjectTypeConverter); - } - return DefaultDecoder.INSTANCE.get(type); + TypeConverter.Factory customTypeConverterFactory = (type, registry) -> { + if (type.equals(ConfigWithNestedInterface.CustomObject.class)) { + return Optional.of(customObjectTypeConverter); } - } + return Optional.empty(); + }; + DefaultDecoder customDecoder = DefaultDecoder.newCustomDecoder(Collections.singletonList(customTypeConverterFactory)); Config config = MapConfig.builder() .put("intValue", "5") .put("customValue", "blah") .build(); - config.setDecoder(new CustomDecoder()); + config.setDecoder(customDecoder); ConfigProxyFactory proxyFactory = new ConfigProxyFactory(config, config.getDecoder(), DefaultPropertyFactory.from(config)); ConfigWithNestedInterface proxy = proxyFactory.newProxy(ConfigWithNestedInterface.class);