From a341ab59b880935bce7e9727657a48af7b7d4173 Mon Sep 17 00:00:00 2001 From: Patrick Strawderman Date: Thu, 22 Feb 2024 12:25:50 -0800 Subject: [PATCH] Avoid class cast exceptions in array and enum converters 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. --- .../converters/ArrayTypeConverterFactory.java | 44 ++++++++-- ...efaultCollectionsTypeConverterFactory.java | 2 +- .../converters/EnumTypeConverterFactory.java | 11 ++- .../netflix/archaius/DefaultDecoderTest.java | 86 ++++++++++++++++++- 4 files changed, 126 insertions(+), 17 deletions(-) diff --git a/archaius2-core/src/main/java/com/netflix/archaius/converters/ArrayTypeConverterFactory.java b/archaius2-core/src/main/java/com/netflix/archaius/converters/ArrayTypeConverterFactory.java index 57c7867e1..cf84f09ca 100644 --- a/archaius2-core/src/main/java/com/netflix/archaius/converters/ArrayTypeConverterFactory.java +++ b/archaius2-core/src/main/java/com/netflix/archaius/converters/ArrayTypeConverterFactory.java @@ -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(); @@ -13,29 +14,56 @@ private ArrayTypeConverterFactory() {} @Override public Optional> 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 elementConverter = (TypeConverter) 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 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 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; }; } } diff --git a/archaius2-core/src/main/java/com/netflix/archaius/converters/DefaultCollectionsTypeConverterFactory.java b/archaius2-core/src/main/java/com/netflix/archaius/converters/DefaultCollectionsTypeConverterFactory.java index 2532f35b9..84a78f1f2 100644 --- a/archaius2-core/src/main/java/com/netflix/archaius/converters/DefaultCollectionsTypeConverterFactory.java +++ b/archaius2-core/src/main/java/com/netflix/archaius/converters/DefaultCollectionsTypeConverterFactory.java @@ -48,7 +48,7 @@ public Optional> 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, diff --git a/archaius2-core/src/main/java/com/netflix/archaius/converters/EnumTypeConverterFactory.java b/archaius2-core/src/main/java/com/netflix/archaius/converters/EnumTypeConverterFactory.java index a22bceec0..9c58aa725 100644 --- a/archaius2-core/src/main/java/com/netflix/archaius/converters/EnumTypeConverterFactory.java +++ b/archaius2-core/src/main/java/com/netflix/archaius/converters/EnumTypeConverterFactory.java @@ -10,18 +10,17 @@ public final class EnumTypeConverterFactory implements TypeConverter.Factory { private EnumTypeConverterFactory() {} + @SuppressWarnings({"unchecked", "rawtypes"}) @Override public Optional> 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 > TypeConverter create(Class clsType) { return value -> Enum.valueOf(clsType, value); } } 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 98a0483e8..4e0988819 100644 --- a/archaius2-core/src/test/java/com/netflix/archaius/DefaultDecoderTest.java +++ b/archaius2-core/src/test/java/com/netflix/archaius/DefaultDecoderTest.java @@ -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; @@ -27,13 +29,24 @@ 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; @@ -41,6 +54,31 @@ public class DefaultDecoderTest { + @SuppressWarnings("unused") // accessed via reflection + private static Collection collectionOfLong; + @SuppressWarnings("unused") // accessed via reflection + private static List listOfInteger; + @SuppressWarnings("unused") // accessed via reflection + private static Set setOfLong; + @SuppressWarnings("unused") // accessed via reflection + private static Map 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; @@ -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 @@ -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> maybeConverter = ArrayTypeConverterFactory.INSTANCE.get(listOfIntegerType, DefaultDecoder.INSTANCE); + Assert.assertFalse(maybeConverter.isPresent()); + } + + @Test + public void testEnumConverterIgnoresParameterizedType() { + Optional> maybeConverter = EnumTypeConverterFactory.INSTANCE.get(listOfIntegerType, DefaultDecoder.INSTANCE); + Assert.assertFalse(maybeConverter.isPresent()); + } + @Test public void testTypeConverterRegistry() { Assert.assertTrue(DefaultDecoder.INSTANCE.get(Instant.class).isPresent());