Skip to content

Commit

Permalink
Merge pull request #53 from catenax-ng/feat/Generic_JsonDeserializer
Browse files Browse the repository at this point in the history
Feat/generic json deserializer
  • Loading branch information
nicoprow authored Feb 23, 2023
2 parents 8904396 + fc57916 commit c418f4c
Show file tree
Hide file tree
Showing 22 changed files with 383 additions and 329 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ The format is based on Keep a Changelog (https://keepachangelog.com/en/1.0.0/),

- BPDM Pool: Update dependencies to mitigate vulnerabilities in old versions

### Changed

- Replaced manual JSON deserializer implementations for various DTOs by generic DataClassUnwrappedJsonDeserializer.

## [3.0.2] - 2022-02-15

### Changed
Expand Down
22 changes: 22 additions & 0 deletions bpdm-common/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,28 @@
<groupId>io.github.microutils</groupId>
<artifactId>kotlin-logging-jvm</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<!-- in kotlin, use mockk instead of mockito -->
<exclusion>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,28 +20,15 @@
package org.eclipse.tractusx.bpdm.common.dto.response

import com.fasterxml.jackson.annotation.JsonUnwrapped
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import io.swagger.v3.oas.annotations.media.Schema
import org.eclipse.tractusx.bpdm.common.service.DataClassUnwrappedJsonDeserializer

@JsonDeserialize(using = AddressBpnResponseDeserializer::class)
@JsonDeserialize(using = DataClassUnwrappedJsonDeserializer::class)
@Schema(name = "AddressBpnResponse", description = "Localized address record of a business partner")
data class AddressBpnResponse(
@Schema(description = "Business Partner Number, main identifier value for addresses")
val bpn: String,
@field:JsonUnwrapped
val address: AddressResponse
)

class AddressBpnResponseDeserializer(vc: Class<AddressBpnResponse>?) : StdDeserializer<AddressBpnResponse>(vc) {
override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): AddressBpnResponse {
val node = parser.codec.readTree<JsonNode>(parser)
return AddressBpnResponse(
node.get(AddressBpnResponse::bpn.name).textValue(),
ctxt.readTreeAsValue(node, AddressResponse::class.java)
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,31 +20,15 @@
package org.eclipse.tractusx.bpdm.common.dto.response

import com.fasterxml.jackson.annotation.JsonUnwrapped
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import io.swagger.v3.oas.annotations.media.Schema
import org.eclipse.tractusx.bpdm.common.service.DataClassUnwrappedJsonDeserializer

@JsonDeserialize(using = AddressPartnerResponse.CustomDeserializer::class)
@JsonDeserialize(using = DataClassUnwrappedJsonDeserializer::class)
@Schema(name = "AddressPartnerResponse", description = "Business partner of type address")
data class AddressPartnerResponse(
@Schema(description = "Business Partner Number of this address")
val bpn: String,
@field:JsonUnwrapped
val properties: AddressResponse
) {
class CustomDeserializer(vc: Class<AddressPartnerResponse>?) : StdDeserializer<AddressPartnerResponse>(vc) {
constructor() : this(null) // for some reason jackson needs this explicit default constructor

override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): AddressPartnerResponse {
val node = parser.codec.readTree<JsonNode>(parser)
return AddressPartnerResponse(
node.get(AddressPartnerResponse::bpn.name).textValue(),
ctxt.readTreeAsValue(node, AddressResponse::class.java)
)
}
}
}

)
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,12 @@
package org.eclipse.tractusx.bpdm.common.dto.response

import com.fasterxml.jackson.annotation.JsonUnwrapped
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.DeserializationContext
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import io.swagger.v3.oas.annotations.media.Schema
import org.eclipse.tractusx.bpdm.common.service.DataClassUnwrappedJsonDeserializer
import java.time.Instant

@JsonDeserialize(using = LegalEntityPartnerResponse.CustomDeserializer::class)
@JsonDeserialize(using = DataClassUnwrappedJsonDeserializer::class)
@Schema(name = "LegalEntityPartnerResponse", description = "Business partner of type legal entity with currentness")
data class LegalEntityPartnerResponse(
@Schema(description = "Business Partner Number of this legal entity")
Expand All @@ -37,17 +34,4 @@ data class LegalEntityPartnerResponse(
val properties: LegalEntityResponse,
@Schema(description = "The timestamp the business partner data was last indicated to be still current")
val currentness: Instant
) {
class CustomDeserializer(vc: Class<LegalEntityPartnerResponse>?) : StdDeserializer<LegalEntityPartnerResponse>(vc) {
constructor() : this(null) // for some reason jackson needs this explicit default constructor

override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): LegalEntityPartnerResponse {
val node = parser.codec.readTree<JsonNode>(parser)
return LegalEntityPartnerResponse(
node.get(LegalEntityPartnerResponse::bpn.name).textValue(),
ctxt.readTreeAsValue(node, LegalEntityResponse::class.java),
ctxt.readTreeAsValue(node.get(LegalEntityPartnerResponse::currentness.name), Instant::class.java)
)
}
}
}
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*******************************************************************************
* Copyright (c) 2021,2023 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available at
* https://www.apache.org/licenses/LICENSE-2.0.
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*
* SPDX-License-Identifier: Apache-2.0
******************************************************************************/

package org.eclipse.tractusx.bpdm.common.service

import com.fasterxml.jackson.annotation.JsonUnwrapped
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.*
import com.fasterxml.jackson.databind.deser.ContextualDeserializer
import kotlin.reflect.KClass
import kotlin.reflect.KFunction
import kotlin.reflect.KType
import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.primaryConstructor
import kotlin.reflect.jvm.javaField
import kotlin.reflect.jvm.javaType

/**
* Data classes using the annotation JsonUnwrapped are not supported out-of-the-box by jackson-module-kotlin and need a custom deserializer.
* This generic JsonDeserializer supports this use case. A new object is initialized via the primary constructor.
*/
class DataClassUnwrappedJsonDeserializer : JsonDeserializer<Any?>(), ContextualDeserializer {
// We need a ContextualDeserializer to find out the destination type.
override fun createContextual(ctxt: DeserializationContext, property: BeanProperty?): JsonDeserializer<*> {
val javaType: JavaType = ctxt.contextualType
?: throw IllegalStateException("ContextualType is missing from DeserializationContext")
return DataClassUnwrappedJsonDeserializerForType(javaType)
}

override fun deserialize(p: JsonParser?, ctxt: DeserializationContext?): Any? {
throw IllegalStateException("DataClassUnwrappedJsonDeserializer.deserialize() can not be used directly")
}
}

private class DataClassUnwrappedJsonDeserializerForType(destinationJavaType: JavaType) : JsonDeserializer<Any>() {
private val destinationClass: KClass<out Any>
private val primaryConstructor: KFunction<Any>
private val constructorParameters: List<ConstructorParameter>

init {
this.destinationClass = destinationJavaType.rawClass.kotlin
this.primaryConstructor = destinationClass.primaryConstructor
?: throw IllegalStateException("Primary constructor required for '$destinationClass'")

// Annotation @field:JsonUnwrapped is stored on the Java field, not the constructor parameter.
val propertiesByName = destinationClass.memberProperties.associateBy { it.name }

this.constructorParameters = primaryConstructor.parameters.map { param ->
val name = param.name
?: throw IllegalStateException("Some primary constructor parameter of '$destinationClass' doesn't have a name")
val type = param.type
val jsonUnwrapped = propertiesByName[name]?.javaField?.getAnnotation(JsonUnwrapped::class.java) != null
ConstructorParameter(name, type, jsonUnwrapped)
}
}

override fun deserialize(parser: JsonParser, ctxt: DeserializationContext): Any {
val rootNode = parser.codec.readTree<JsonNode>(parser)

val constructorValues = constructorParameters.map { param ->
val jacksonType = ctxt.typeFactory.constructType(param.type.javaType)
val node = if (param.jsonUnwrapped) rootNode else rootNode.get(param.name)
val value = readTreeAsValue(ctxt, node, jacksonType)
if (value == null && !param.type.isMarkedNullable)
throw IllegalArgumentException("Field '${param.name}' of '$destinationClass' is required")
value
}

return primaryConstructor.call(args = constructorValues.toTypedArray())
}

private fun readTreeAsValue(ctxt: DeserializationContext, node: JsonNode?, javaType: JavaType) : Any? =
if (node == null || node.isNull) null
else ctxt.readTreeAsValue<Any?>(node, javaType)
}

private data class ConstructorParameter(
val name: String,
val type: KType,
val jsonUnwrapped: Boolean,
)
Loading

0 comments on commit c418f4c

Please sign in to comment.