Skip to content

Commit d5b619b

Browse files
authored
Merge pull request #12 from linked-planet/feature/gson
add GsonUtil that provides a GsonBuilder compatible with the model, otherwise every consumer has to solve the sealed class mapping themselves
2 parents 70f2170 + 8509b3f commit d5b619b

File tree

5 files changed

+471
-173
lines changed

5 files changed

+471
-173
lines changed

kotlin-insight-client/kotlin-insight-client-api/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,11 @@
2828
<version>2.0.1.Final</version>
2929
<scope>provided</scope>
3030
</dependency>
31+
<dependency>
32+
<groupId>com.google.code.gson</groupId>
33+
<artifactId>gson</artifactId>
34+
<version>2.2.2-atlassian-1</version>
35+
<scope>provided</scope>
36+
</dependency>
3137
</dependencies>
3238
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*-
2+
* #%L
3+
* kotlin-insight-client-api
4+
* %%
5+
* Copyright (C) 2022 - 2023 linked-planet GmbH
6+
* %%
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
* #L%
19+
*/
20+
package com.linkedplanet.kotlininsightclient.api.impl
21+
22+
import com.google.gson.GsonBuilder
23+
import com.google.gson.TypeAdapter
24+
import com.google.gson.stream.JsonReader
25+
import com.google.gson.stream.JsonWriter
26+
import com.linkedplanet.kotlininsightclient.api.model.InsightAttribute
27+
import com.linkedplanet.kotlininsightclient.api.model.ObjectTypeSchemaAttribute
28+
import java.time.ZonedDateTime
29+
30+
31+
class GsonUtil {
32+
companion object {
33+
fun gsonBuilder(): GsonBuilder = GsonBuilder()
34+
.registerTypeAdapter(ZonedDateTime::class.java, zonedDateTimeAdapter)
35+
.registerTypeAdapterFactory(SealedTypeAdapterFactory.of(InsightAttribute::class))
36+
.registerTypeAdapterFactory(SealedTypeAdapterFactory.of(ObjectTypeSchemaAttribute::class,
37+
typeFieldName = "type",
38+
jsonNameForType = { it.simpleName!!.removeSuffix("Schema") to it }
39+
))
40+
41+
42+
private val zonedDateTimeAdapter = object : TypeAdapter<ZonedDateTime>() {
43+
override fun write(out: JsonWriter, value: ZonedDateTime?) {
44+
out.value(value.toString())
45+
}
46+
47+
override fun read(`in`: JsonReader): ZonedDateTime {
48+
return ZonedDateTime.parse(`in`.nextString())
49+
}
50+
}
51+
}
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*-
2+
* #%L
3+
* kotlin-insight-client-api
4+
* %%
5+
* Copyright (C) 2022 - 2023 linked-planet GmbH
6+
* %%
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
* #L%
19+
*/
20+
package com.linkedplanet.kotlininsightclient.api.impl
21+
22+
import com.google.gson.Gson
23+
import com.google.gson.JsonElement
24+
import com.google.gson.JsonObject
25+
import com.google.gson.JsonParseException
26+
import com.google.gson.JsonPrimitive
27+
import com.google.gson.TypeAdapter
28+
import com.google.gson.TypeAdapterFactory
29+
import com.google.gson.reflect.TypeToken
30+
import com.google.gson.stream.JsonReader
31+
import com.google.gson.stream.JsonWriter
32+
import kotlin.reflect.KClass
33+
34+
/**
35+
* Creates TypeAdaptors for sealed classes.
36+
* Especially for deserialization Gson needs to know which class to instantiate.
37+
*
38+
* @param baseType The parent sealed class
39+
* @param typeFieldName the additional field inside the json that contains the name of the type
40+
* @param jsonNameForType Choose a different name for the type einside the json, other than the name of the class itself (defaults to simplename)
41+
*/
42+
class SealedTypeAdapterFactory<T : Any> private constructor(
43+
private val baseType: KClass<T>,
44+
private val typeFieldName: String,
45+
jsonNameForType: (KClass<out T>) -> Pair<String, KClass<out T>>
46+
) : TypeAdapterFactory {
47+
48+
private val subclasses = baseType.sealedSubclasses
49+
private val nameToSubclass: Map<String, KClass<out T>> = subclasses.associate(jsonNameForType)
50+
51+
init {
52+
if (!baseType.isSealed) throw IllegalArgumentException("$baseType is not a sealed class")
53+
}
54+
55+
override fun <R : Any> create(gson: Gson, type: TypeToken<R>?): TypeAdapter<R>? {
56+
if (type == null || subclasses.isEmpty() || subclasses.none { type.rawType.isAssignableFrom(it.java) }) return null
57+
val elementTypeAdapter = gson.getAdapter(JsonElement::class.java)
58+
val subclassToDelegate: Map<KClass<*>, TypeAdapter<*>> = subclasses.associateWith {
59+
gson.getDelegateAdapter(this, TypeToken.get(it.java))
60+
}
61+
62+
return object : TypeAdapter<R>() {
63+
64+
override fun write(writer: JsonWriter, value: R) {
65+
val srcType = value::class
66+
val label = srcType.simpleName!!
67+
68+
@Suppress("UNCHECKED_CAST") val delegate = subclassToDelegate[srcType] as TypeAdapter<R>
69+
val jsonObject = delegate.toJsonTree(value).asJsonObject
70+
71+
val clone = JsonObject()
72+
if (!jsonObject.has(typeFieldName)) {
73+
clone.add(typeFieldName, JsonPrimitive(label))
74+
}
75+
jsonObject.entrySet().forEach {
76+
clone.add(it.key, it.value)
77+
}
78+
elementTypeAdapter.write(writer, clone)
79+
}
80+
81+
override fun read(reader: JsonReader): R {
82+
val element = elementTypeAdapter.read(reader)
83+
val labelElement = element.asJsonObject.remove(typeFieldName) ?: throw JsonParseException(
84+
"cannot deserialize $baseType because it does not define a field named $typeFieldName"
85+
)
86+
val name = labelElement.asString
87+
val subclass = nameToSubclass[name] ?: throw JsonParseException("cannot find $name subclass of $baseType")
88+
@Suppress("UNCHECKED_CAST")
89+
return (subclass.objectInstance as? R) ?: (subclassToDelegate[subclass]!!.fromJsonTree(element) as R)
90+
}
91+
92+
}
93+
}
94+
95+
companion object {
96+
fun <T : Any> of(
97+
clz: KClass<T>,
98+
typeFieldName: String = "type",
99+
jsonNameForType: (KClass<out T>) -> Pair<String, KClass<out T>> = { it.simpleName!! to it }
100+
) = SealedTypeAdapterFactory(clz, typeFieldName, jsonNameForType)
101+
}
102+
}

0 commit comments

Comments
 (0)