Skip to content

Commit

Permalink
Avoid class cast exceptions in array and enum converters
Browse files Browse the repository at this point in the history
If a ParameterizedType was passed to ArrayTypeConverterFactory or EnumTypeConverterFactory,
a ClassCastException would be thrown when it attempted to unconditionally cast it to Class;
update the factories to handle this more gracefully by just returning Optional.empty().

Additionally, update ArrayTypeConverterFactory to handle arrays of primitives, and add
unit tests for Enums, arrays, and collections.
  • Loading branch information
kilink committed Feb 22, 2024
1 parent f8de50c commit a341ab5
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.lang.reflect.Array;
import java.lang.reflect.Type;
import java.util.Optional;
import java.util.function.ObjIntConsumer;

public final class ArrayTypeConverterFactory implements TypeConverter.Factory {
public static final ArrayTypeConverterFactory INSTANCE = new ArrayTypeConverterFactory();
Expand All @@ -13,29 +14,56 @@ private ArrayTypeConverterFactory() {}

@Override
public Optional<TypeConverter<?>> get(Type type, TypeConverter.Registry registry) {
Class clsType = (Class) type;

if (clsType.isArray()) {
TypeConverter elementConverter = registry.get(clsType.getComponentType()).orElseThrow(() -> new RuntimeException());
if (type instanceof Class<?> && ((Class<?>) type).isArray()) {
Class<?> clsType = (Class<?>) type;
Class<?> elementType = clsType.getComponentType();
@SuppressWarnings("unchecked")
TypeConverter<Object> elementConverter = (TypeConverter<Object>) registry.get(elementType)
.orElseThrow(() -> new RuntimeException("No converter found for array element type '" + elementType + "'"));
return Optional.of(create(elementConverter, clsType.getComponentType()));
}

return Optional.empty();
}

private static TypeConverter<?> create(TypeConverter elementConverter, Class type) {
private static TypeConverter<?> create(TypeConverter<Object> elementConverter, Class<?> type) {
return value -> {
value = value.trim();
if (value.isEmpty()) {
return Array.newInstance(type, 0);
}
String[] elements = value.split(",");
Object[] ar = (Object[]) Array.newInstance(type, elements.length);
Object resultArray = Array.newInstance(type, elements.length);

final ObjIntConsumer<String> elementHandler;
if (type.isPrimitive()) {
if (type.equals(int.class)) {
elementHandler = (s, idx) -> Array.setInt(resultArray, idx, (int) elementConverter.convert(s));
} else if (type.equals(long.class)) {
elementHandler = (s, idx) -> Array.setLong(resultArray, idx, (long) elementConverter.convert(s));
} else if (type.equals(short.class)) {
elementHandler = (s, idx) -> Array.setShort(resultArray, idx, (short) elementConverter.convert(s));
} else if (type.equals(byte.class)) {
elementHandler = (s, idx) -> Array.setByte(resultArray, idx, (byte) elementConverter.convert(s));
} else if (type.equals(char.class)) {
elementHandler = (s, idx) -> Array.setChar(resultArray, idx, (char) elementConverter.convert(s));
} else if (type.equals(boolean.class)) {
elementHandler = (s, idx) -> Array.setBoolean(resultArray, idx, (boolean) elementConverter.convert(s));
} else if (type.equals(float.class)) {
elementHandler = (s, idx) -> Array.setFloat(resultArray, idx, (float) elementConverter.convert(s));
} else if (type.equals(double.class)) {
elementHandler = (s, idx) -> Array.setDouble(resultArray, idx, (double) elementConverter.convert(s));
} else {
throw new UnsupportedOperationException("Unknown primitive type: " + type);
}
} else {
elementHandler = (s, idx) -> Array.set(resultArray, idx, elementConverter.convert(s));
}

for (int i = 0; i < elements.length; i++) {
ar[i] = elementConverter.convert(elements[i]);
elementHandler.accept(elements[i], i);
}
return ar;
return resultArray;
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public Optional<TypeConverter<?>> get(Type type, TypeConverter.Registry registry
TreeSet::new,
Collections::emptySortedSet,
Collections::unmodifiableSortedSet));
} else if (parameterizedType.getRawType().equals(List.class)) {
} else if (parameterizedType.getRawType().equals(List.class) || parameterizedType.getRawType().equals(Collection.class)) {
return Optional.of(createCollectionTypeConverter(
parameterizedType.getActualTypeArguments()[0],
registry,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,17 @@ public final class EnumTypeConverterFactory implements TypeConverter.Factory {

private EnumTypeConverterFactory() {}

@SuppressWarnings({"unchecked", "rawtypes"})
@Override
public Optional<TypeConverter<?>> get(Type type, TypeConverter.Registry registry) {
Class clsType = (Class) type;

if (clsType.isEnum()) {
return Optional.of(create(clsType));
if (type instanceof Class<?> && ((Class<?>) type).isEnum()) {
Class enumClass = (Class<?>) type;
return Optional.of(create(enumClass));
}

return Optional.empty();
}

private static TypeConverter<?> create(Class clsType) {
private static <T extends Enum<T>> TypeConverter<T> create(Class<T> clsType) {
return value -> Enum.valueOf(clsType, value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
*/
package com.netflix.archaius;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URI;
Expand All @@ -27,20 +29,56 @@
import java.time.OffsetTime;
import java.time.Period;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.Currency;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;

import com.netflix.archaius.api.Decoder;
import com.netflix.archaius.api.TypeConverter;
import com.netflix.archaius.converters.ArrayTypeConverterFactory;
import com.netflix.archaius.converters.EnumTypeConverterFactory;
import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.junit.Assert;
import org.junit.Test;


public class DefaultDecoderTest {
@SuppressWarnings("unused") // accessed via reflection
private static Collection<Long> collectionOfLong;
@SuppressWarnings("unused") // accessed via reflection
private static List<Integer> listOfInteger;
@SuppressWarnings("unused") // accessed via reflection
private static Set<Long> setOfLong;
@SuppressWarnings("unused") // accessed via reflection
private static Map<String, Integer> mapOfStringToInteger;

private static final ParameterizedType collectionOfLongType;
private static final ParameterizedType listOfIntegerType;
private static final ParameterizedType setOfLongType;
private static final ParameterizedType mapofStringToIntegerType;

static {
try {
collectionOfLongType = (ParameterizedType) DefaultDecoderTest.class.getDeclaredField("collectionOfLong").getGenericType();
listOfIntegerType = (ParameterizedType) DefaultDecoderTest.class.getDeclaredField("listOfInteger").getGenericType();
setOfLongType = (ParameterizedType) DefaultDecoderTest.class.getDeclaredField("setOfLong").getGenericType();
mapofStringToIntegerType = (ParameterizedType) DefaultDecoderTest.class.getDeclaredField("mapOfStringToInteger").getGenericType();
} catch (NoSuchFieldException exc) {
throw new AssertionError("listOfString field not found", exc);
}
}

@Test
public void testJavaNumbers() {
DefaultDecoder decoder = DefaultDecoder.INSTANCE;
Expand All @@ -58,8 +96,8 @@ public void testJavaNumbers() {
Assert.assertEquals(Double.valueOf(Double.MAX_VALUE), decoder.decode(Double.class, String.valueOf(Double.MAX_VALUE)));
Assert.assertEquals(BigInteger.valueOf(Long.MAX_VALUE), decoder.decode(BigInteger.class, String.valueOf(Long.MAX_VALUE)));
Assert.assertEquals(BigDecimal.valueOf(Double.MAX_VALUE), decoder.decode(BigDecimal.class, String.valueOf(Double.MAX_VALUE)));
Assert.assertEquals(new AtomicInteger(Integer.MAX_VALUE).intValue(), decoder.decode(AtomicInteger.class, String.valueOf(Integer.MAX_VALUE)).intValue());
Assert.assertEquals(new AtomicLong(Long.MAX_VALUE).longValue(), decoder.decode(AtomicLong.class, String.valueOf(Long.MAX_VALUE)).longValue());
Assert.assertEquals(Integer.MAX_VALUE, decoder.decode(AtomicInteger.class, String.valueOf(Integer.MAX_VALUE)).get());
Assert.assertEquals(Long.MAX_VALUE, decoder.decode(AtomicLong.class, String.valueOf(Long.MAX_VALUE)).get());
}

@Test
Expand Down Expand Up @@ -89,6 +127,50 @@ public void testJavaMiscellaneous() throws DecoderException {
Assert.assertEquals(Locale.ENGLISH, decoder.decode(Locale.class, "en"));
}

@Test
public void testCollections() {
Decoder decoder = DefaultDecoder.INSTANCE;
Assert.assertEquals(Collections.emptyList(), decoder.decode(listOfIntegerType, ""));
Assert.assertEquals(Arrays.asList(1, 2, 3, 4, 5, 6), decoder.decode(listOfIntegerType, "1,2,3,4,5,6"));
Assert.assertEquals(Arrays.asList(1L, 2L, 3L, 4L, 5L, 6L), decoder.decode(collectionOfLongType, "1,2,3,4,5,6"));
Assert.assertEquals(Collections.singleton(2L), decoder.decode(setOfLongType, "2,2,2,2"));
Assert.assertEquals(Collections.emptyMap(), decoder.decode(mapofStringToIntegerType, ""));
Assert.assertEquals(Collections.singletonMap("key", 12345), decoder.decode(mapofStringToIntegerType, "key=12345"));
}

@Test
public void testArrays() {
DefaultDecoder decoder = DefaultDecoder.INSTANCE;
Assert.assertArrayEquals(new String[] { "foo", "bar", "baz" }, decoder.decode(String[].class, "foo,bar,baz"));
Assert.assertArrayEquals(new Integer[] {1, 2, 3, 4, 5}, decoder.decode(Integer[].class, "1,2,3,4,5"));
Assert.assertArrayEquals(new int[] {1, 2, 3, 4, 5}, decoder.decode(int[].class, "1,2,3,4,5"));
Assert.assertArrayEquals(new Integer[0], decoder.decode(Integer[].class, ""));
Assert.assertArrayEquals(new int[0], decoder.decode(int[].class, ""));
Assert.assertArrayEquals(new Long[] {1L, 2L, 3L, 4L, 5L}, decoder.decode(Long[].class, "1,2,3,4,5"));
Assert.assertArrayEquals(new long[] {1L, 2L, 3L, 4L, 5L}, decoder.decode(long[].class, "1,2,3,4,5"));
Assert.assertArrayEquals(new Long[0], decoder.decode(Long[].class, ""));
Assert.assertArrayEquals(new long[0], decoder.decode(long[].class, ""));
}

enum TestEnumType { FOO, BAR, BAZ }
@Test
public void testEnum() {
Decoder decoder = DefaultDecoder.INSTANCE;
Assert.assertEquals(TestEnumType.FOO, decoder.decode((Type) TestEnumType.class, "FOO"));
}

@Test
public void testArrayConverterIgnoresParameterizedType() {
Optional<TypeConverter<?>> maybeConverter = ArrayTypeConverterFactory.INSTANCE.get(listOfIntegerType, DefaultDecoder.INSTANCE);
Assert.assertFalse(maybeConverter.isPresent());
}

@Test
public void testEnumConverterIgnoresParameterizedType() {
Optional<TypeConverter<?>> maybeConverter = EnumTypeConverterFactory.INSTANCE.get(listOfIntegerType, DefaultDecoder.INSTANCE);
Assert.assertFalse(maybeConverter.isPresent());
}

@Test
public void testTypeConverterRegistry() {
Assert.assertTrue(DefaultDecoder.INSTANCE.get(Instant.class).isPresent());
Expand Down

0 comments on commit a341ab5

Please sign in to comment.