diff --git a/graphql-dgs-platform/build.gradle.kts b/graphql-dgs-platform/build.gradle.kts index 93af574cb..2dbc989c1 100644 --- a/graphql-dgs-platform/build.gradle.kts +++ b/graphql-dgs-platform/build.gradle.kts @@ -90,10 +90,10 @@ dependencies { version { require("3.4.22") } } // CVEs - api("org.apache.logging.log4j:log4j-to-slf4j:2.22.0") { + api("org.apache.logging.log4j:log4j-to-slf4j:2.22.1") { because("Refer to CVE-2021-44228; https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-44228") } - api("org.apache.logging.log4j:log4j-api:2.22.0") { + api("org.apache.logging.log4j:log4j-api:2.22.1") { because("Refer to CVE-2021-44228; https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-44228") } } diff --git a/graphql-dgs-spring-boot-micrometer/build.gradle.kts b/graphql-dgs-spring-boot-micrometer/build.gradle.kts index 69e4d39b9..d9e9f5840 100644 --- a/graphql-dgs-spring-boot-micrometer/build.gradle.kts +++ b/graphql-dgs-spring-boot-micrometer/build.gradle.kts @@ -4,7 +4,7 @@ dependencies { implementation("net.bytebuddy:byte-buddy") implementation("io.micrometer:micrometer-core") implementation("commons-codec:commons-codec") - implementation("com.netflix.spectator:spectator-api:1.6.+") + implementation("com.netflix.spectator:spectator-api:1.7.+") implementation("com.github.ben-manes.caffeine:caffeine") implementation("org.springframework:spring-context-support") diff --git a/graphql-dgs/src/main/kotlin/com/netflix/graphql/dgs/internal/DefaultInputObjectMapper.kt b/graphql-dgs/src/main/kotlin/com/netflix/graphql/dgs/internal/DefaultInputObjectMapper.kt index 3d6f08449..a9ab57edb 100644 --- a/graphql-dgs/src/main/kotlin/com/netflix/graphql/dgs/internal/DefaultInputObjectMapper.kt +++ b/graphql-dgs/src/main/kotlin/com/netflix/graphql/dgs/internal/DefaultInputObjectMapper.kt @@ -20,23 +20,54 @@ import com.netflix.graphql.dgs.exceptions.DgsInvalidInputArgumentException import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.core.KotlinDetector +import org.springframework.core.ResolvableType +import org.springframework.core.convert.ConversionException +import org.springframework.core.convert.TypeDescriptor +import org.springframework.core.convert.converter.ConditionalGenericConverter +import org.springframework.core.convert.converter.GenericConverter +import org.springframework.core.convert.support.DefaultConversionService import org.springframework.util.CollectionUtils import org.springframework.util.ReflectionUtils import java.lang.reflect.Field -import java.lang.reflect.ParameterizedType -import java.lang.reflect.Type -import java.lang.reflect.TypeVariable -import java.lang.reflect.WildcardType import kotlin.reflect.KClass import kotlin.reflect.KParameter -import kotlin.reflect.KType import kotlin.reflect.full.primaryConstructor import kotlin.reflect.jvm.javaType -import kotlin.reflect.jvm.jvmErasure -@Suppress("UNCHECKED_CAST") -class DefaultInputObjectMapper(private val customInputObjectMapper: InputObjectMapper? = null) : InputObjectMapper { - private val logger: Logger = LoggerFactory.getLogger(InputObjectMapper::class.java) +class DefaultInputObjectMapper(customInputObjectMapper: InputObjectMapper? = null) : InputObjectMapper { + + companion object { + private val logger: Logger = LoggerFactory.getLogger(InputObjectMapper::class.java) + } + + private val conversionService = DefaultConversionService() + + init { + conversionService.addConverter(Converter(customInputObjectMapper ?: this)) + } + + private class Converter(private val mapper: InputObjectMapper) : ConditionalGenericConverter { + override fun getConvertibleTypes(): Set { + return setOf(GenericConverter.ConvertiblePair(Map::class.java, Any::class.java)) + } + + override fun matches(sourceType: TypeDescriptor, targetType: TypeDescriptor): Boolean { + if (sourceType.isMap) { + val keyDescriptor = sourceType.mapKeyTypeDescriptor + return keyDescriptor == null || keyDescriptor.type == String::class.java + } + return false + } + + override fun convert(source: Any?, sourceType: TypeDescriptor, targetType: TypeDescriptor): Any? { + @Suppress("UNCHECKED_CAST") + val sourceMap = source as Map + if (KotlinDetector.isKotlinType(targetType.type)) { + return mapper.mapToKotlinObject(sourceMap, targetType.type.kotlin) + } + return mapper.mapToJavaObject(sourceMap, targetType.type) + } + } override fun mapToKotlinObject(inputMap: Map, targetClass: KClass): T { val constructor = targetClass.primaryConstructor @@ -57,44 +88,13 @@ class DefaultInputObjectMapper(private val customInputObjectMapper: InputObjectM } val input = inputMap[parameter.name] - - if (input is Map<*, *>) { - val nestedTarget = parameter.type.jvmErasure - val subValue = if (isObjectOrAny(nestedTarget)) { - input - } else if (KotlinDetector.isKotlinType(nestedTarget.java)) { - customInputObjectMapper?.mapToKotlinObject(input as Map, nestedTarget) - ?: mapToKotlinObject(input as Map, nestedTarget) - } else { - customInputObjectMapper?.mapToJavaObject(input as Map, nestedTarget.java) - ?: mapToJavaObject(input as Map, nestedTarget.java) - } - parametersByName[parameter] = subValue - } else if (parameter.type.jvmErasure.java.isEnum && input !== null) { - val enumValue = - (parameter.type.jvmErasure.java.enumConstants as Array>).find { enumValue -> enumValue.name == input } - parametersByName[parameter] = enumValue - } else if (input is List<*>) { - val newList = convertList( - input = input, - targetClass = targetClass.java, - nestedClass = parameter.type.arguments[0].type!!.jvmErasure, - nestedType = - if (parameter.type.arguments[0].type!!.arguments.isNotEmpty()) { - ((parameter.type.arguments[0].type!!.arguments[0].type) as KType).javaType - } else { - null - } - ) - - if (parameter.type.jvmErasure == Set::class) { - parametersByName[parameter] = newList.toSet() - } else { - parametersByName[parameter] = newList - } - } else { - parametersByName[parameter] = input + val typeDescriptor = TypeDescriptor(ResolvableType.forType(parameter.type.javaType), null, null) + val convertedValue = try { + conversionService.convert(input, typeDescriptor) + } catch (exc: ConversionException) { + throw throw DgsInvalidInputArgumentException("Failed to convert value $input to $typeDescriptor", exc) } + parametersByName[parameter] = convertedValue } return try { @@ -105,50 +105,29 @@ class DefaultInputObjectMapper(private val customInputObjectMapper: InputObjectM } override fun mapToJavaObject(inputMap: Map, targetClass: Class): T { - if (targetClass == Object::class.java || targetClass == Map::class.java) { + if (targetClass.isAssignableFrom(inputMap::class.java)) { + @Suppress("UNCHECKED_CAST") return inputMap as T } val ctor = ReflectionUtils.accessibleConstructor(targetClass) - ReflectionUtils.makeAccessible(ctor) val instance = ctor.newInstance() var nrOfFieldErrors = 0 - inputMap.forEach { - val declaredField = ReflectionUtils.findField(targetClass, it.key) - if (declaredField != null) { - val fieldType = getFieldType(declaredField, targetClass) - // resolve the field class we will map into, as well as an optional type argument in case such - // class is a parameterized type, such as a List. - val (fieldClass: Class<*>, fieldArgumentType: Type?) = when (fieldType) { - is ParameterizedType -> fieldType.rawType as Class<*> to fieldType.actualTypeArguments[0] - is Class<*> -> fieldType to null - else -> Class.forName(fieldType.typeName) to null - } - - if (it.value is Map<*, *>) { - val mappedValue = if (KotlinDetector.isKotlinType(fieldClass)) { - mapToKotlinObject(it.value as Map, fieldClass.kotlin) - } else { - mapToJavaObject(it.value as Map, fieldClass) - } - trySetField(declaredField, instance, mappedValue) - } else if (it.value is List<*>) { - val newList = convertList(it.value as List<*>, targetClass, fieldClass.kotlin, fieldArgumentType) - if (declaredField.type == Set::class.java) { - trySetField(declaredField, instance, newList.toSet()) - } else { - trySetField(declaredField, instance, newList) - } - } else if (fieldClass.isEnum) { - val enumValue = if (it.value == null) null else (fieldClass.enumConstants as Array>).find { enumValue -> enumValue.name == it.value.toString() } - trySetField(declaredField, instance, enumValue) - } else { - trySetField(declaredField, instance, it.value) - } - } else { - logger.warn("Field '${it.key}' was not found on Input object of type '$targetClass'") + for ((name, value) in inputMap.entries) { + val field = ReflectionUtils.findField(targetClass, name) + if (field == null) { nrOfFieldErrors++ + logger.warn("Field '{}' was not found on Input object of type '{}'", name, targetClass) + continue + } + + val fieldType = TypeDescriptor(ResolvableType.forField(field, targetClass), null, null) + val convertedValue = try { + conversionService.convert(value, fieldType) + } catch (exc: ConversionException) { + throw DgsInvalidInputArgumentException("Failed to convert value $value to $fieldType", exc) } + trySetField(field, instance, convertedValue) } /** @@ -165,78 +144,10 @@ class DefaultInputObjectMapper(private val customInputObjectMapper: InputObjectM private fun trySetField(declaredField: Field, instance: Any?, value: Any?) { try { - declaredField.isAccessible = true - declaredField.set(instance, value) + ReflectionUtils.makeAccessible(declaredField) + ReflectionUtils.setField(declaredField, instance, value) } catch (ex: Exception) { throw DgsInvalidInputArgumentException("Invalid input argument `$value` for field `${declaredField.name}` on type `${instance?.javaClass?.name}`") } } - - private fun getFieldType(field: Field, targetClass: Class<*>): Type { - val genericSuperclass = targetClass.genericSuperclass - val fieldType: Type = field.genericType - return if (fieldType is ParameterizedType && fieldType.actualTypeArguments.size == 1) { - fieldType.actualTypeArguments[0] - } else if (genericSuperclass is ParameterizedType && field.type != field.genericType) { - val typeParameters = (genericSuperclass.rawType as Class<*>).typeParameters - val indexOfTypeParameter = typeParameters.indexOfFirst { it.name == fieldType.typeName } - genericSuperclass.actualTypeArguments[indexOfTypeParameter] - } else { - field.type - } - } - - private fun convertList( - input: List<*>, - targetClass: Class<*>, - nestedClass: KClass<*>, - nestedType: Type? = null - ): List<*> { - val mappedList = input.filterNotNull().map { listItem -> - if (listItem is List<*>) { - when (nestedType) { - is ParameterizedType -> - convertList( - listItem, - targetClass, - (nestedType.rawType as Class<*>).kotlin, - nestedType.actualTypeArguments[0] - ) - is TypeVariable<*> -> { - val indexOfGeneric = - ((targetClass.genericSuperclass as ParameterizedType).rawType as Class<*>) - .typeParameters.indexOfFirst { it.name == nestedType.typeName } - val parameterType = - (targetClass.genericSuperclass as ParameterizedType).actualTypeArguments[indexOfGeneric] - convertList(listItem, targetClass, (parameterType as Class<*>).kotlin) - } - is WildcardType -> { - // We are assuming that the upper-bound type is a Class and not a Parametrized Type. - convertList(listItem, targetClass, (nestedType.upperBounds[0] as Class<*>).kotlin) - } - is Class<*> -> - convertList(listItem, targetClass, nestedType.kotlin) - else -> - listItem - } - } else if (nestedClass.java.isEnum) { - (nestedClass.java.enumConstants as Array>).first { it.name == listItem } - } else if (listItem is Map<*, *>) { - if (isObjectOrAny(nestedClass)) { - listItem - } else if (KotlinDetector.isKotlinType(nestedClass.java)) { - mapToKotlinObject(listItem as Map, nestedClass) - } else { - mapToJavaObject(listItem as Map, nestedClass.java) - } - } else { - listItem - } - } - - return mappedList - } - - private fun isObjectOrAny(nestedTarget: KClass<*>) = - nestedTarget.java == Object::class.java || nestedTarget == Any::class } diff --git a/graphql-dgs/src/test/kotlin/com/netflix/graphql/dgs/internal/InputObjectMapperTest.kt b/graphql-dgs/src/test/kotlin/com/netflix/graphql/dgs/internal/InputObjectMapperTest.kt index 54acc0702..184cee970 100644 --- a/graphql-dgs/src/test/kotlin/com/netflix/graphql/dgs/internal/InputObjectMapperTest.kt +++ b/graphql-dgs/src/test/kotlin/com/netflix/graphql/dgs/internal/InputObjectMapperTest.kt @@ -156,28 +156,6 @@ internal class InputObjectMapperTest { assertThat(mapToObject.simpleString).isEqualTo("hello") } - @Test - fun `An input argument of the wrong type should throw a DgsInvalidArgumentException for a Java object`() { - val newInput = input.toMutableMap() - // Use an Int as input where a String was expected - newInput["simpleString"] = 1 - - assertThatThrownBy { inputObjectMapper.mapToJavaObject(newInput, JInputObject::class.java) }.isInstanceOf( - DgsInvalidInputArgumentException::class.java - ).hasMessageStartingWith("Invalid input argument `1` for field `simpleString` on type `com.netflix.graphql.dgs.internal.java.test.inputobjects.JInputObject`") - } - - @Test - fun `An input argument of the wrong type should throw a DgsInvalidArgumentException for a Kotlin object`() { - val newInput = input.toMutableMap() - // Use an Int as input where a String was expected - newInput["simpleString"] = 1 - - assertThatThrownBy { inputObjectMapper.mapToKotlinObject(newInput, KotlinInputObject::class) }.isInstanceOf( - DgsInvalidInputArgumentException::class.java - ).hasMessageStartingWith("Provided input arguments") - } - @Test fun `A list argument should be able to convert to Set in Kotlin`() { val input = mapOf("items" to listOf(1, 2, 3)) @@ -256,6 +234,19 @@ internal class InputObjectMapperTest { assertThat(result.string).isEqualTo("default") } + @Test + fun `mapping to an object with a Kotlin class works when there is a field with an enum type`() { + val result = inputObjectMapper.mapToKotlinObject(mapOf("name" to "the-name", "type" to "BAR"), KotlinObjectWithEnumField::class) + assertThat(result.name).isEqualTo("the-name") + assertThat(result.type).isEqualTo(FieldType.BAR) + } + + @Test + fun `mapping to an object works when the input type can be converted to the target type`() { + val result = inputObjectMapper.mapToKotlinObject(mapOf("items" to listOf("1", "2", "3", "4")), KotlinObjectWithSet::class) + assertThat(result.items).isEqualTo(setOf(1, 2, 3, 4)) + } + data class KotlinInputObject(val simpleString: String?, val someDate: LocalDateTime, val someObject: KotlinSomeObject) data class KotlinNestedInputObject(val input: KotlinInputObject) data class KotlinDoubleNestedInputObject(val inputL1: KotlinNestedInputObject) @@ -265,4 +256,7 @@ internal class InputObjectMapperTest { data class KotlinObjectWithMap(val json: Map) data class KotlinWithJavaProperty(val name: String, val objectProperty: JInputObject) + + enum class FieldType { FOO, BAR, BAZ } + data class KotlinObjectWithEnumField(val name: String, val type: FieldType) }