5 changed files with 333 additions and 1 deletions
@ -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 @@ |
|||||||
|
/* |
||||||
|
* 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