Skip to content

Commit

Permalink
Merge pull request #702 from Netflix/custom-converters
Browse files Browse the repository at this point in the history
Add a Decoder that accepts custom TypeConverters
  • Loading branch information
akang31 authored Feb 14, 2024
2 parents 2446891 + b7ebce5 commit f8de50c
Show file tree
Hide file tree
Showing 6 changed files with 211 additions and 138 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package com.netflix.archaius;

import com.netflix.archaius.api.Decoder;
import com.netflix.archaius.api.TypeConverter;
import com.netflix.archaius.exceptions.ParseException;

import java.lang.reflect.Constructor;
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.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;

/**
* A {@code Decoder} implementation that also implements {@code TypeConverter.Registry}, and delegates to a supplied
* collection of converter factories.
*/
abstract class AbstractRegistryDecoder implements Decoder, TypeConverter.Registry {

private final Map<Type, TypeConverter<?>> cache = new ConcurrentHashMap<>();

private final List<TypeConverter.Factory> factories;

AbstractRegistryDecoder(Collection<? extends TypeConverter.Factory> factories) {
this.factories = Collections.unmodifiableList(new ArrayList<>(factories));
}

@Override
public <T> T decode(Class<T> type, String encoded) {
return decode((Type) type, encoded);
}

@Override
public <T> T decode(Type type, String encoded) {
try {
if (encoded == null) {
return null;
}
@SuppressWarnings("unchecked")
TypeConverter<T> converter = (TypeConverter<T>) getOrCreateConverter(type);
if (converter == null) {
throw new RuntimeException("No converter found for type '" + type + "'");
}
return converter.convert(encoded);
} catch (Exception e) {
throw new ParseException("Error decoding type `" + type.getTypeName() + "`", e);
}
}

@Override
public Optional<TypeConverter<?>> get(Type type) {
return Optional.ofNullable(getOrCreateConverter(type));
}

private TypeConverter<?> getOrCreateConverter(Type type) {
TypeConverter<?> converter = cache.get(type);
if (converter == null) {
converter = resolve(type);
if (converter == null) {
return null;
}
TypeConverter<?> existing = cache.putIfAbsent(type, converter);
if (existing != null) {
converter = existing;
}
}
return converter;
}

/**
* Iterate through all TypeConverter#Factory's and return the first TypeConverter that matches
* @param type
* @return
*/
private TypeConverter<?> resolve(Type type) {
return factories.stream()
.flatMap(factory -> factory.get(type, this).map(Stream::of).orElseGet(Stream::empty))
.findFirst()
.orElseGet(() -> findValueOfTypeConverter(type));
}

/**
* @param type
* @param <T>
* @return Return a converter that uses reflection on either a static valueOf or ctor(String) to convert a string value to the
* type. Will return null if neither is found
*/
private static <T> TypeConverter<T> findValueOfTypeConverter(Type type) {
if (!(type instanceof Class)) {
return null;
}

@SuppressWarnings("unchecked")
Class<T> cls = (Class<T>) type;

// Next look a valueOf(String) static method
Method method;
try {
method = cls.getMethod("valueOf", String.class);
return value -> {
try {
return (T) method.invoke(null, value);
} catch (Exception e) {
throw new ParseException("Error converting value '" + value + "' to '" + type.getTypeName() + "'", e);
}
};
} catch (NoSuchMethodException e1) {
// Next look for a T(String) constructor
Constructor<T> c;
try {
c = cls.getConstructor(String.class);
return value -> {
try {
return (T) c.newInstance(value);
} catch (Exception e) {
throw new ParseException("Error converting value", e);
}
};
} catch (NoSuchMethodException e) {
return null;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.netflix.archaius;

import com.netflix.archaius.api.TypeConverter;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Objects;

/**
* A configurable {@code Decoder} implementation which allows extension through custom {@code TypeConverter.Factory}
* instances. The custom factories are searched first, and if no appropriate converter is found, the default
* converters used by {@code DefaultDecoder} will be consulted.
*/
public class CustomDecoder extends AbstractRegistryDecoder {
private CustomDecoder(Collection<? extends TypeConverter.Factory> typeConverterFactories) {
super(typeConverterFactories);
}

/**
* Create a new {@code CustomDecoder} 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 customTypeConverterFactories the collection of converter factories to use for this decoder
*/
public static CustomDecoder create(Collection<? extends TypeConverter.Factory> customTypeConverterFactories) {
Objects.requireNonNull(customTypeConverterFactories, "customTypeConverterFactories == null");
List<TypeConverter.Factory> factories = new ArrayList<>(customTypeConverterFactories);
factories.addAll(DefaultDecoder.DEFAULT_FACTORIES);
return new CustomDecoder(factories);
}
}
125 changes: 9 additions & 116 deletions archaius2-core/src/main/java/com/netflix/archaius/DefaultDecoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,134 +15,27 @@
*/
package com.netflix.archaius;

import com.netflix.archaius.api.Decoder;
import com.netflix.archaius.api.TypeConverter;
import com.netflix.archaius.converters.ArrayTypeConverterFactory;
import com.netflix.archaius.converters.DefaultCollectionsTypeConverterFactory;
import com.netflix.archaius.converters.DefaultTypeConverterFactory;
import com.netflix.archaius.converters.EnumTypeConverterFactory;
import com.netflix.archaius.exceptions.ParseException;

import javax.inject.Singleton;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;

@Singleton
public class DefaultDecoder implements Decoder, TypeConverter.Registry {
private final Map<Type, TypeConverter<?>> cache = new ConcurrentHashMap<>();

private final List<TypeConverter.Factory> factories = new ArrayList<>();

public class DefaultDecoder extends AbstractRegistryDecoder {
public static final List<TypeConverter.Factory> DEFAULT_FACTORIES = Collections.unmodifiableList(Arrays.asList(
DefaultTypeConverterFactory.INSTANCE,
DefaultCollectionsTypeConverterFactory.INSTANCE,
ArrayTypeConverterFactory.INSTANCE,
EnumTypeConverterFactory.INSTANCE));
public static final DefaultDecoder INSTANCE = new DefaultDecoder();

private DefaultDecoder() {
factories.add(DefaultTypeConverterFactory.INSTANCE);
factories.add(DefaultCollectionsTypeConverterFactory.INSTANCE);
factories.add(ArrayTypeConverterFactory.INSTANCE);
factories.add(EnumTypeConverterFactory.INSTANCE);
}

@Override
public <T> T decode(Class<T> type, String encoded) {
return decode((Type)type, encoded);
}

@Override
public <T> T decode(Type type, String encoded) {
try {
if (encoded == null) {
return null;
}
@SuppressWarnings("unchecked")
TypeConverter<T> converter = (TypeConverter<T>) getOrCreateConverter(type);
if (converter == null) {
throw new RuntimeException("No converter found for type '" + type + "'");
}
return converter.convert(encoded);
} catch (Exception e) {
throw new ParseException("Error decoding type `" + type.getTypeName() + "`", e);
}
}

@Override
public Optional<TypeConverter<?>> get(Type type) {
return Optional.ofNullable(getOrCreateConverter(type));
}

private TypeConverter<?> getOrCreateConverter(Type type) {
TypeConverter<?> converter = cache.get(type);
if (converter == null) {
converter = resolve(type);
if (converter == null) {
return null;
}
TypeConverter<?> existing = cache.putIfAbsent(type, converter);
if (existing != null) {
converter = existing;
}
}
return converter;
}

/**
* Iterate through all TypeConverter#Factory's and return the first TypeConverter that matches
* @param type
* @return
*/
private TypeConverter<?> resolve(Type type) {
return factories.stream()
.flatMap(factory -> factory.get(type, this).map(Stream::of).orElseGet(Stream::empty))
.findFirst()
.orElseGet(() -> findValueOfTypeConverter(type));
}

/**
* @param type
* @param <T>
* @return Return a converter that uses reflection on either a static valueOf or ctor(String) to convert a string value to the
* type. Will return null if neither is found
*/
private static <T> TypeConverter<T> findValueOfTypeConverter(Type type) {
if (!(type instanceof Class)) {
return null;
}

@SuppressWarnings("unchecked")
Class<T> cls = (Class<T>) type;

// Next look a valueOf(String) static method
Method method;
try {
method = cls.getMethod("valueOf", String.class);
return value -> {
try {
return (T) method.invoke(null, value);
} catch (Exception e) {
throw new ParseException("Error converting value '" + value + "' to '" + type.getTypeName() + "'", e);
}
};
} catch (NoSuchMethodException e1) {
// Next look for a T(String) constructor
Constructor<T> c;
try {
c = cls.getConstructor(String.class);
return value -> {
try {
return (T) c.newInstance(value);
} catch (Exception e) {
throw new ParseException("Error converting value", e);
}
};
} catch (NoSuchMethodException e) {
return null;
}
}
super(DEFAULT_FACTORIES);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.netflix.archaius;

import com.netflix.archaius.api.TypeConverter;
import org.junit.Assert;
import org.junit.Test;

import java.lang.reflect.Type;
import java.util.Collections;
import java.util.Optional;

public class CustomDecoderTest {

@Test
public void testCustomTypeConverters() {
TypeConverter<String> stringConverter = String::toUpperCase;
TypeConverter<Long> 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();
};
CustomDecoder decoder = CustomDecoder.create(Collections.singletonList(factory));
Assert.assertEquals("FOO", decoder.decode((Type) CharSequence.class, "foo"));
Assert.assertEquals("FOO", decoder.decode((Type) String.class, "foo"));
// default is overridden
Assert.assertEquals(Long.valueOf(6), decoder.decode((Type) Long.class, "3"));
// default converter is used
Assert.assertEquals(Integer.valueOf(3), decoder.decode((Type) Integer.class, "3"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
*/
package com.netflix.archaius;


import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URI;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.nullValue;

import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
Expand Down Expand Up @@ -593,31 +592,18 @@ interface CustomObject {
@Test
public void testNestedInterfaceWithCustomDecoder() {
TypeConverter<ConfigWithNestedInterface.CustomObject> customObjectTypeConverter = value -> value::toUpperCase;

final class CustomDecoder implements Decoder, TypeConverter.Registry {
@Override
public <T> T decode(Class<T> 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<TypeConverter<?>> 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();
};
Decoder customDecoder = CustomDecoder.create(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);
Expand Down

0 comments on commit f8de50c

Please sign in to comment.