Skip to content

Commit

Permalink
Merge branch 'master' into unknown-operation-fix
Browse files Browse the repository at this point in the history
  • Loading branch information
kilink authored Jan 9, 2024
2 parents b9b3479 + 6117c0d commit a1b3dc1
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 177 deletions.
4 changes: 2 additions & 2 deletions graphql-dgs-platform/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
Expand Down
2 changes: 1 addition & 1 deletion graphql-dgs-spring-boot-micrometer/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<GenericConverter.ConvertiblePair> {
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<String, *>
if (KotlinDetector.isKotlinType(targetType.type)) {
return mapper.mapToKotlinObject(sourceMap, targetType.type.kotlin)
}
return mapper.mapToJavaObject(sourceMap, targetType.type)
}
}

override fun <T : Any> mapToKotlinObject(inputMap: Map<String, *>, targetClass: KClass<T>): T {
val constructor = targetClass.primaryConstructor
Expand All @@ -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<String, *>, nestedTarget)
?: mapToKotlinObject(input as Map<String, *>, nestedTarget)
} else {
customInputObjectMapper?.mapToJavaObject(input as Map<String, *>, nestedTarget.java)
?: mapToJavaObject(input as Map<String, *>, nestedTarget.java)
}
parametersByName[parameter] = subValue
} else if (parameter.type.jvmErasure.java.isEnum && input !== null) {
val enumValue =
(parameter.type.jvmErasure.java.enumConstants as Array<Enum<*>>).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 {
Expand All @@ -105,50 +105,29 @@ class DefaultInputObjectMapper(private val customInputObjectMapper: InputObjectM
}

override fun <T> mapToJavaObject(inputMap: Map<String, *>, targetClass: Class<T>): 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<String, *>, fieldClass.kotlin)
} else {
mapToJavaObject(it.value as Map<String, *>, 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<Enum<*>>).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)
}

/**
Expand All @@ -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<Enum<*>>).first { it.name == listItem }
} else if (listItem is Map<*, *>) {
if (isObjectOrAny(nestedClass)) {
listItem
} else if (KotlinDetector.isKotlinType(nestedClass.java)) {
mapToKotlinObject(listItem as Map<String, *>, nestedClass)
} else {
mapToJavaObject(listItem as Map<String, *>, nestedClass.java)
}
} else {
listItem
}
}

return mappedList
}

private fun isObjectOrAny(nestedTarget: KClass<*>) =
nestedTarget.java == Object::class.java || nestedTarget == Any::class
}
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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)
Expand All @@ -265,4 +256,7 @@ internal class InputObjectMapperTest {
data class KotlinObjectWithMap(val json: Map<String, Any>)

data class KotlinWithJavaProperty(val name: String, val objectProperty: JInputObject)

enum class FieldType { FOO, BAR, BAZ }
data class KotlinObjectWithEnumField(val name: String, val type: FieldType)
}

0 comments on commit a1b3dc1

Please sign in to comment.