5 changed files with 333 additions and 1 deletions
@ -0,0 +1,109 @@
@@ -0,0 +1,109 @@
|
||||
/* |
||||
* Copyright 2002-2020 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License 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. |
||||
*/ |
||||
|
||||
package org.springframework.messaging.converter; |
||||
|
||||
import java.io.IOException; |
||||
import java.io.Reader; |
||||
import java.io.Writer; |
||||
import java.lang.reflect.Type; |
||||
import java.util.Map; |
||||
|
||||
import kotlinx.serialization.KSerializer; |
||||
import kotlinx.serialization.SerializersKt; |
||||
import kotlinx.serialization.json.Json; |
||||
|
||||
import org.springframework.util.ConcurrentReferenceHashMap; |
||||
import org.springframework.util.FileCopyUtils; |
||||
|
||||
/** |
||||
* Implementation of {@link MessageConverter} that can read and write JSON |
||||
* using <a href="https://github.com/Kotlin/kotlinx.serialization">kotlinx.serialization</a>. |
||||
* |
||||
* <p>This converter can be used to bind {@code @Serializable} Kotlin classes. |
||||
* |
||||
* @author Sebastien Deleuze |
||||
* @since 5.3 |
||||
*/ |
||||
public class KotlinSerializationJsonMessageConverter extends AbstractJsonMessageConverter { |
||||
|
||||
private static final Map<Type, KSerializer<Object>> serializerCache = new ConcurrentReferenceHashMap<>(); |
||||
|
||||
private final Json json; |
||||
|
||||
|
||||
/** |
||||
* Construct a new {@code KotlinSerializationJsonMessageConverter} with default configuration. |
||||
*/ |
||||
public KotlinSerializationJsonMessageConverter() { |
||||
this(Json.Default); |
||||
} |
||||
|
||||
/** |
||||
* Construct a new {@code KotlinSerializationJsonMessageConverter} with the given delegate. |
||||
* @param json the Json instance to use |
||||
*/ |
||||
public KotlinSerializationJsonMessageConverter(Json json) { |
||||
this.json = json; |
||||
} |
||||
|
||||
@Override |
||||
protected Object fromJson(Reader reader, Type resolvedType) { |
||||
try { |
||||
return fromJson(FileCopyUtils.copyToString(reader), resolvedType); |
||||
} |
||||
catch (IOException ex) { |
||||
throw new MessageConversionException("Could not read JSON: " + ex.getMessage(), ex); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
protected Object fromJson(String payload, Type resolvedType) { |
||||
return this.json.decodeFromString(serializer(resolvedType), payload); |
||||
} |
||||
|
||||
@Override |
||||
protected void toJson(Object payload, Type resolvedType, Writer writer) { |
||||
try { |
||||
writer.write(toJson(payload, resolvedType).toCharArray()); |
||||
} |
||||
catch (IOException ex) { |
||||
throw new MessageConversionException("Could not write JSON: " + ex.getMessage(), ex); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
protected String toJson(Object payload, Type resolvedType) { |
||||
return this.json.encodeToString(serializer(resolvedType), payload); |
||||
} |
||||
|
||||
/** |
||||
* Tries to find a serializer that can marshall or unmarshall instances of the given type |
||||
* using kotlinx.serialization. If no serializer can be found, an exception is thrown. |
||||
* <p>Resolved serializers are cached and cached results are returned on successive calls. |
||||
* @param type the type to find a serializer for |
||||
* @return a resolved serializer for the given type |
||||
* @throws RuntimeException if no serializer supporting the given type can be found |
||||
*/ |
||||
private KSerializer<Object> serializer(Type type) { |
||||
KSerializer<Object> serializer = serializerCache.get(type); |
||||
if (serializer == null) { |
||||
serializer = SerializersKt.serializer(type); |
||||
serializerCache.put(type, serializer); |
||||
} |
||||
return serializer; |
||||
} |
||||
} |
||||
@ -0,0 +1,214 @@
@@ -0,0 +1,214 @@
|
||||
/* |
||||
* Copyright 2002-2019 the original author or authors. |
||||
* |
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
* you may not use this file except in compliance with the License. |
||||
* You may obtain a copy of the License 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. |
||||
*/ |
||||
|
||||
package org.springframework.messaging.converter |
||||
|
||||
import kotlinx.serialization.Serializable |
||||
import org.assertj.core.api.Assertions |
||||
import org.junit.jupiter.api.Test |
||||
import org.springframework.core.MethodParameter |
||||
import org.springframework.messaging.support.MessageBuilder |
||||
import java.nio.charset.StandardCharsets |
||||
import kotlin.reflect.typeOf |
||||
|
||||
@Suppress("UsePropertyAccessSyntax") |
||||
class KotlinSerializationJsonMessageConverterTests { |
||||
|
||||
private val converter = KotlinSerializationJsonMessageConverter() |
||||
|
||||
@Test |
||||
fun readObject() { |
||||
val payload = """ |
||||
{ |
||||
"bytes": [ |
||||
1, |
||||
2 |
||||
], |
||||
"array": [ |
||||
"Foo", |
||||
"Bar" |
||||
], |
||||
"number": 42, |
||||
"string": "Foo", |
||||
"bool": true, |
||||
"fraction": 42 |
||||
} |
||||
""".trimIndent() |
||||
val message = MessageBuilder.withPayload(payload.toByteArray(StandardCharsets.UTF_8)).build() |
||||
val result = converter.fromMessage(message, SerializableBean::class.java) as SerializableBean |
||||
|
||||
Assertions.assertThat(result.bytes).containsExactly(0x1, 0x2) |
||||
Assertions.assertThat(result.array).containsExactly("Foo", "Bar") |
||||
Assertions.assertThat(result.number).isEqualTo(42) |
||||
Assertions.assertThat(result.string).isEqualTo("Foo") |
||||
Assertions.assertThat(result.bool).isTrue() |
||||
Assertions.assertThat(result.fraction).isEqualTo(42.0f) |
||||
} |
||||
|
||||
@Test |
||||
@Suppress("UNCHECKED_CAST") |
||||
fun readArrayOfObjects() { |
||||
val payload = """ |
||||
[ |
||||
{ |
||||
"bytes": [ |
||||
1, |
||||
2 |
||||
], |
||||
"array": [ |
||||
"Foo", |
||||
"Bar" |
||||
], |
||||
"number": 42, |
||||
"string": "Foo", |
||||
"bool": true, |
||||
"fraction": 42 |
||||
} |
||||
] |
||||
""".trimIndent() |
||||
val message = MessageBuilder.withPayload(payload.toByteArray(StandardCharsets.UTF_8)).build() |
||||
val result = converter.fromMessage(message, Array<SerializableBean>::class.java) as Array<SerializableBean> |
||||
|
||||
Assertions.assertThat(result).hasSize(1) |
||||
Assertions.assertThat(result[0].bytes).containsExactly(0x1, 0x2) |
||||
Assertions.assertThat(result[0].array).containsExactly("Foo", "Bar") |
||||
Assertions.assertThat(result[0].number).isEqualTo(42) |
||||
Assertions.assertThat(result[0].string).isEqualTo("Foo") |
||||
Assertions.assertThat(result[0].bool).isTrue() |
||||
Assertions.assertThat(result[0].fraction).isEqualTo(42.0f) |
||||
} |
||||
|
||||
@Test |
||||
@Suppress("UNCHECKED_CAST") |
||||
@ExperimentalStdlibApi |
||||
fun readGenericCollection() { |
||||
val payload = """ |
||||
[ |
||||
{ |
||||
"bytes": [ |
||||
1, |
||||
2 |
||||
], |
||||
"array": [ |
||||
"Foo", |
||||
"Bar" |
||||
], |
||||
"number": 42, |
||||
"string": "Foo", |
||||
"bool": true, |
||||
"fraction": 42 |
||||
} |
||||
] |
||||
""".trimIndent() |
||||
val method = javaClass.getDeclaredMethod("handleList", List::class.java) |
||||
val param = MethodParameter(method, 0) |
||||
val message = MessageBuilder.withPayload(payload.toByteArray(StandardCharsets.UTF_8)).build() |
||||
val result = converter.fromMessage(message, typeOf<List<SerializableBean>>()::class.java, param) as List<SerializableBean> |
||||
|
||||
Assertions.assertThat(result).hasSize(1) |
||||
Assertions.assertThat(result[0].bytes).containsExactly(0x1, 0x2) |
||||
Assertions.assertThat(result[0].array).containsExactly("Foo", "Bar") |
||||
Assertions.assertThat(result[0].number).isEqualTo(42) |
||||
Assertions.assertThat(result[0].string).isEqualTo("Foo") |
||||
Assertions.assertThat(result[0].bool).isTrue() |
||||
Assertions.assertThat(result[0].fraction).isEqualTo(42.0f) |
||||
} |
||||
|
||||
@Test |
||||
fun readFailsOnInvalidJson() { |
||||
val payload = """ |
||||
this is an invalid JSON document |
||||
""".trimIndent() |
||||
|
||||
val message = MessageBuilder.withPayload(payload.toByteArray(StandardCharsets.UTF_8)).build() |
||||
Assertions.assertThatExceptionOfType(MessageConversionException::class.java).isThrownBy { |
||||
converter.fromMessage(message, SerializableBean::class.java) |
||||
} |
||||
} |
||||
|
||||
@Test |
||||
fun writeObject() { |
||||
val serializableBean = SerializableBean(byteArrayOf(0x1, 0x2), arrayOf("Foo", "Bar"), 42, "Foo", true, 42.0f) |
||||
val message = converter.toMessage(serializableBean, null) |
||||
val result = String((message!!.payload as ByteArray), StandardCharsets.UTF_8) |
||||
|
||||
Assertions.assertThat(result) |
||||
.contains("\"bytes\":[1,2]") |
||||
.contains("\"array\":[\"Foo\",\"Bar\"]") |
||||
.contains("\"number\":42") |
||||
.contains("\"string\":\"Foo\"") |
||||
.contains("\"bool\":true") |
||||
.contains("\"fraction\":42.0") |
||||
} |
||||
|
||||
@Test |
||||
fun writeObjectWithNullableProperty() { |
||||
val serializableBean = SerializableBean(byteArrayOf(0x1, 0x2), arrayOf("Foo", "Bar"), 42, null, true, 42.0f) |
||||
val message = converter.toMessage(serializableBean, null) |
||||
val result = String((message!!.payload as ByteArray), StandardCharsets.UTF_8) |
||||
|
||||
Assertions.assertThat(result) |
||||
.contains("\"bytes\":[1,2]") |
||||
.contains("\"array\":[\"Foo\",\"Bar\"]") |
||||
.contains("\"number\":42") |
||||
.contains("\"string\":null") |
||||
.contains("\"bool\":true") |
||||
.contains("\"fraction\":42.0") |
||||
} |
||||
|
||||
@Test |
||||
fun writeArrayOfObjects() { |
||||
val serializableBean = SerializableBean(byteArrayOf(0x1, 0x2), arrayOf("Foo", "Bar"), 42, "Foo", true, 42.0f) |
||||
val expectedJson = """ |
||||
[{"bytes":[1,2],"array":["Foo","Bar"],"number":42,"string":"Foo","bool":true,"fraction":42.0}] |
||||
""".trimIndent() |
||||
|
||||
val message = converter.toMessage(arrayOf(serializableBean), null) |
||||
val result = String((message!!.payload as ByteArray), StandardCharsets.UTF_8) |
||||
|
||||
Assertions.assertThat(result).isEqualTo(expectedJson) |
||||
} |
||||
|
||||
@Test |
||||
@ExperimentalStdlibApi |
||||
fun writeGenericCollection() { |
||||
val serializableBean = SerializableBean(byteArrayOf(0x1, 0x2), arrayOf("Foo", "Bar"), 42, "Foo", true, 42.0f) |
||||
val expectedJson = """ |
||||
[{"bytes":[1,2],"array":["Foo","Bar"],"number":42,"string":"Foo","bool":true,"fraction":42.0}] |
||||
""".trimIndent() |
||||
|
||||
val method = javaClass.getDeclaredMethod("handleList", List::class.java) |
||||
val param = MethodParameter(method, 0) |
||||
val message = converter.toMessage(arrayListOf(serializableBean), null, param) |
||||
val result = String((message!!.payload as ByteArray), StandardCharsets.UTF_8) |
||||
|
||||
Assertions.assertThat(result).isEqualTo(expectedJson) |
||||
} |
||||
|
||||
@Suppress("UNUSED_PARAMETER") |
||||
fun handleList(payload: List<SerializableBean>) {} |
||||
|
||||
@Serializable |
||||
@Suppress("ArrayInDataClass") |
||||
data class SerializableBean( |
||||
val bytes: ByteArray, |
||||
val array: Array<String>, |
||||
val number: Int, |
||||
val string: String?, |
||||
val bool: Boolean, |
||||
val fraction: Float |
||||
) |
||||
} |
||||
Loading…
Reference in new issue