diff --git a/spring-web/spring-web.gradle b/spring-web/spring-web.gradle index ca787381de..aeb1989c38 100644 --- a/spring-web/spring-web.gradle +++ b/spring-web/spring-web.gradle @@ -54,7 +54,9 @@ dependencies { optional("org.apache.groovy:groovy") optional("org.jetbrains.kotlin:kotlin-reflect") optional("org.jetbrains.kotlin:kotlin-stdlib") + optional("org.jetbrains.kotlinx:kotlinx-serialization-cbor") optional("org.jetbrains.kotlinx:kotlinx-serialization-json") + optional("org.jetbrains.kotlinx:kotlinx-serialization-protobuf") testImplementation(testFixtures(project(":spring-beans"))) testImplementation(testFixtures(project(":spring-context"))) testImplementation(testFixtures(project(":spring-core"))) diff --git a/spring-web/src/main/java/org/springframework/http/MediaType.java b/spring-web/src/main/java/org/springframework/http/MediaType.java index 102dafc1f9..5a4a6a55cf 100644 --- a/spring-web/src/main/java/org/springframework/http/MediaType.java +++ b/spring-web/src/main/java/org/springframework/http/MediaType.java @@ -217,6 +217,18 @@ public class MediaType extends MimeType implements Serializable { */ public static final String APPLICATION_PROBLEM_XML_VALUE = "application/problem+xml"; + /** + * Public constant media type for {@code application/x-protobuf}. + * @since 6.0 + */ + public static final MediaType APPLICATION_PROTOBUF; + + /** + * A String equivalent of {@link MediaType#APPLICATION_PROTOBUF}. + * @since 6.0 + */ + public static final String APPLICATION_PROTOBUF_VALUE = "application/x-protobuf"; + /** * Public constant media type for {@code application/rss+xml}. * @since 4.3.6 @@ -418,6 +430,7 @@ public class MediaType extends MimeType implements Serializable { APPLICATION_PROBLEM_JSON = new MediaType("application", "problem+json"); APPLICATION_PROBLEM_JSON_UTF8 = new MediaType("application", "problem+json", StandardCharsets.UTF_8); APPLICATION_PROBLEM_XML = new MediaType("application", "problem+xml"); + APPLICATION_PROTOBUF = new MediaType("application", "x-protobuf"); APPLICATION_RSS_XML = new MediaType("application", "rss+xml"); APPLICATION_STREAM_JSON = new MediaType("application", "stream+json"); APPLICATION_XHTML_XML = new MediaType("application", "xhtml+xml"); diff --git a/spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java b/spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java index 0f2d51af10..a27b0709a3 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java +++ b/spring-web/src/main/java/org/springframework/http/codec/CodecConfigurer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -176,6 +176,22 @@ public interface CodecConfigurer { */ void jaxb2Encoder(Encoder encoder); + /** + * Override the default Kotlin Serialization CBOR {@code Decoder}. + * @param decoder the decoder instance to use + * @since 6.0 + * @see org.springframework.http.codec.cbor.KotlinSerializationCborDecoder + */ + void kotlinSerializationCborDecoder(Decoder decoder); + + /** + * Override the default Kotlin Serialization CBOR {@code Encoder}. + * @param encoder the encoder instance to use + * @since 6.0 + * @see org.springframework.http.codec.cbor.KotlinSerializationCborDecoder + */ + void kotlinSerializationCborEncoder(Encoder encoder); + /** * Override the default Kotlin Serialization JSON {@code Decoder}. * @param decoder the decoder instance to use @@ -192,6 +208,22 @@ public interface CodecConfigurer { */ void kotlinSerializationJsonEncoder(Encoder encoder); + /** + * Override the default Kotlin Serialization Protobuf {@code Decoder}. + * @param decoder the decoder instance to use + * @since 6.0 + * @see org.springframework.http.codec.protobuf.KotlinSerializationProtobufDecoder + */ + void kotlinSerializationProtobufDecoder(Decoder decoder); + + /** + * Override the default Kotlin Serialization Protobuf {@code Encoder}. + * @param encoder the encoder instance to use + * @since 6.0 + * @see org.springframework.http.codec.protobuf.KotlinSerializationProtobufEncoder + */ + void kotlinSerializationProtobufEncoder(Encoder encoder); + /** * Register a consumer to apply to default config instances. This can be * used to configure rather than replace a specific codec or multiple diff --git a/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationBinaryDecoder.java b/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationBinaryDecoder.java new file mode 100644 index 0000000000..6ecffdd0e5 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationBinaryDecoder.java @@ -0,0 +1,116 @@ +/* + * Copyright 2002-2022 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.http.codec; + +import java.util.List; +import java.util.Map; + +import kotlinx.serialization.BinaryFormat; +import kotlinx.serialization.KSerializer; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.ByteArrayDecoder; +import org.springframework.core.codec.Decoder; +import org.springframework.core.codec.DecodingException; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.lang.Nullable; +import org.springframework.util.MimeType; + +/** + * Abstract base class for {@link Decoder} implementations that defer to Kotlin + * {@linkplain BinaryFormat binary serializers}. + * + * @author Sebastien Deleuze + * @author Iain Henderson + * @author Arjen Poutsma + * @since 6.0 + * @param the type of {@link BinaryFormat} + */ +public abstract class KotlinSerializationBinaryDecoder extends KotlinSerializationSupport + implements Decoder { + + // Byte array decoding needed for now, see https://github.com/Kotlin/kotlinx.serialization/issues/204 for more details + private final ByteArrayDecoder byteArrayDecoder = new ByteArrayDecoder(); + + + public KotlinSerializationBinaryDecoder(T format, MimeType... supportedMimeTypes) { + super(format, supportedMimeTypes); + } + + /** + * Configure a limit on the number of bytes that can be buffered whenever + * the input stream needs to be aggregated. This can be a result of + * decoding to a single {@code DataBuffer}, + * {@link java.nio.ByteBuffer ByteBuffer}, {@code byte[]}, + * {@link org.springframework.core.io.Resource Resource}, {@code String}, etc. + * It can also occur when splitting the input stream, e.g. delimited text, + * in which case the limit applies to data buffered between delimiters. + *

By default this is set to 256K. + * @param byteCount the max number of bytes to buffer, or -1 for unlimited + */ + public void setMaxInMemorySize(int byteCount) { + this.byteArrayDecoder.setMaxInMemorySize(byteCount); + } + + /** + * Return the {@link #setMaxInMemorySize configured} byte count limit. + */ + public int getMaxInMemorySize() { + return this.byteArrayDecoder.getMaxInMemorySize(); + } + + @Override + public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) { + return canSerialize(elementType, mimeType); + } + + @Override + public List getDecodableMimeTypes() { + return supportedMimeTypes(); + } + + @Override + public List getDecodableMimeTypes(ResolvableType targetType) { + return supportedMimeTypes(); + } + + @Override + public Flux decode(Publisher inputStream, ResolvableType elementType, + @Nullable MimeType mimeType, + @Nullable Map hints) { + + return Flux.error(new UnsupportedOperationException()); + } + + @Override + public Mono decodeToMono(Publisher inputStream, ResolvableType elementType, + @Nullable MimeType mimeType, @Nullable Map hints) { + return Mono.defer(() -> { + KSerializer serializer = serializer(elementType); + if (serializer == null) { + return Mono.error(new DecodingException("Could not find KSerializer for " + elementType)); + } + return this.byteArrayDecoder + .decodeToMono(inputStream, elementType, mimeType, hints) + .map(byteArray -> format().decodeFromByteArray(serializer, byteArray)); + }); + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationBinaryEncoder.java b/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationBinaryEncoder.java new file mode 100644 index 0000000000..de16320454 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationBinaryEncoder.java @@ -0,0 +1,101 @@ +/* + * Copyright 2002-2022 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.http.codec; + +import java.util.List; +import java.util.Map; + +import kotlinx.serialization.BinaryFormat; +import kotlinx.serialization.KSerializer; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.ByteArrayEncoder; +import org.springframework.core.codec.Encoder; +import org.springframework.core.codec.EncodingException; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.lang.Nullable; +import org.springframework.util.MimeType; + +/** + * Abstract base class for {@link Encoder} implementations that defer to Kotlin + * {@linkplain BinaryFormat binary serializers}. + * + * @author Sebastien Deleuze + * @author Iain Henderson + * @author Arjen Poutsma + * @since 6.0 + * @param the type of {@link BinaryFormat} + */ +public abstract class KotlinSerializationBinaryEncoder extends KotlinSerializationSupport + implements Encoder { + + // ByteArraySequence encoding needed for now, see https://github.com/Kotlin/kotlinx.serialization/issues/204 for more details + private final ByteArrayEncoder byteArrayEncoder = new ByteArrayEncoder(); + + protected KotlinSerializationBinaryEncoder(T format, MimeType... supportedMimeTypes) { + super(format, supportedMimeTypes); + } + + @Override + public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) { + return canSerialize(elementType, mimeType); + } + + @Override + public List getEncodableMimeTypes() { + return supportedMimeTypes(); + } + + @Override + public List getEncodableMimeTypes(ResolvableType elementType) { + return supportedMimeTypes(); + } + + @Override + public Flux encode(Publisher inputStream, DataBufferFactory bufferFactory, + ResolvableType elementType, + @Nullable MimeType mimeType, @Nullable Map hints) { + if (inputStream instanceof Mono) { + return Mono.from(inputStream) + .map(value -> encodeValue(value, bufferFactory, elementType, mimeType, hints)) + .flux(); + } + else { + ResolvableType listType = ResolvableType.forClassWithGenerics(List.class, elementType); + return Flux.from(inputStream) + .collectList() + .map(list -> encodeValue(list, bufferFactory, listType, mimeType, hints)) + .flux(); + } + } + + @Override + public DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory, ResolvableType valueType, + @Nullable MimeType mimeType, @Nullable Map hints) { + + KSerializer serializer = serializer(valueType); + if (serializer == null) { + throw new EncodingException("Could not find KSerializer for " + valueType); + } + byte[] bytes = format().encodeToByteArray(serializer, value); + return this.byteArrayEncoder.encodeValue(bytes, bufferFactory, valueType, mimeType, null); + } +} diff --git a/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationStringDecoder.java b/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationStringDecoder.java new file mode 100644 index 0000000000..ef68256339 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationStringDecoder.java @@ -0,0 +1,116 @@ +/* + * Copyright 2002-2022 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.http.codec; + +import java.util.List; +import java.util.Map; + +import kotlinx.serialization.KSerializer; +import kotlinx.serialization.StringFormat; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.Decoder; +import org.springframework.core.codec.DecodingException; +import org.springframework.core.codec.StringDecoder; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.lang.Nullable; +import org.springframework.util.MimeType; + +/** + * Abstract base class for {@link Decoder} implementations that defer to Kotlin + * {@linkplain StringFormat string serializers}. + * + * @author Sebastien Deleuze + * @author Iain Henderson + * @author Arjen Poutsma + * @since 6.0 + * @param the type of {@link StringFormat} + */ +public abstract class KotlinSerializationStringDecoder extends KotlinSerializationSupport + implements Decoder { + + // String decoding needed for now, see https://github.com/Kotlin/kotlinx.serialization/issues/204 for more details + private final StringDecoder stringDecoder = StringDecoder.allMimeTypes(StringDecoder.DEFAULT_DELIMITERS, false); + + + public KotlinSerializationStringDecoder(T format, MimeType... supportedMimeTypes) { + super(format, supportedMimeTypes); + } + + /** + * Configure a limit on the number of bytes that can be buffered whenever + * the input stream needs to be aggregated. This can be a result of + * decoding to a single {@code DataBuffer}, + * {@link java.nio.ByteBuffer ByteBuffer}, {@code byte[]}, + * {@link org.springframework.core.io.Resource Resource}, {@code String}, etc. + * It can also occur when splitting the input stream, e.g. delimited text, + * in which case the limit applies to data buffered between delimiters. + *

By default this is set to 256K. + * @param byteCount the max number of bytes to buffer, or -1 for unlimited + */ + public void setMaxInMemorySize(int byteCount) { + this.stringDecoder.setMaxInMemorySize(byteCount); + } + + /** + * Return the {@link #setMaxInMemorySize configured} byte count limit. + */ + public int getMaxInMemorySize() { + return this.stringDecoder.getMaxInMemorySize(); + } + + @Override + public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) { + return canSerialize(elementType, mimeType); + } + + @Override + public List getDecodableMimeTypes() { + return supportedMimeTypes(); + } + + @Override + public List getDecodableMimeTypes(ResolvableType targetType) { + return supportedMimeTypes(); + } + + @Override + public Flux decode(Publisher inputStream, ResolvableType elementType, + @Nullable MimeType mimeType, + @Nullable Map hints) { + return Flux.error(new UnsupportedOperationException()); + } + + @Override + public Mono decodeToMono(Publisher inputStream, ResolvableType elementType, + @Nullable MimeType mimeType, @Nullable Map hints) { + + return Mono.defer(() -> { + KSerializer serializer = serializer(elementType); + if (serializer == null) { + return Mono.error(new DecodingException("Could not find KSerializer for " + elementType)); + } + return this.stringDecoder + .decodeToMono(inputStream, elementType, mimeType, hints) + .map(string -> format().decodeFromString(serializer, string)); + }); + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationStringEncoder.java b/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationStringEncoder.java new file mode 100644 index 0000000000..e5a360ac0d --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationStringEncoder.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2022 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.http.codec; + +import java.util.List; +import java.util.Map; + +import kotlinx.serialization.KSerializer; +import kotlinx.serialization.StringFormat; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import org.springframework.core.ResolvableType; +import org.springframework.core.codec.CharSequenceEncoder; +import org.springframework.core.codec.Encoder; +import org.springframework.core.codec.EncodingException; +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.core.io.buffer.DataBufferFactory; +import org.springframework.lang.Nullable; +import org.springframework.util.MimeType; + +/** + * Abstract base class for {@link Encoder} implementations that defer to Kotlin + * {@linkplain StringFormat string serializers}. + * + * @author Sebastien Deleuze + * @author Iain Henderson + * @author Arjen Poutsma + * @since 6.0 + * @param the type of {@link StringFormat} + */ +public abstract class KotlinSerializationStringEncoder extends KotlinSerializationSupport + implements Encoder { + + // CharSequence encoding needed for now, see https://github.com/Kotlin/kotlinx.serialization/issues/204 for more details + private final CharSequenceEncoder charSequenceEncoder = CharSequenceEncoder.allMimeTypes(); + + protected KotlinSerializationStringEncoder(T format, MimeType... supportedMimeTypes) { + super(format, supportedMimeTypes); + } + + @Override + public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) { + return canSerialize(elementType, mimeType); + } + + @Override + public List getEncodableMimeTypes() { + return supportedMimeTypes(); + } + + @Override + public List getEncodableMimeTypes(ResolvableType elementType) { + return supportedMimeTypes(); + } + + + @Override + public Flux encode(Publisher inputStream, DataBufferFactory bufferFactory, + ResolvableType elementType, + @Nullable MimeType mimeType, @Nullable Map hints) { + if (inputStream instanceof Mono) { + return Mono.from(inputStream) + .map(value -> encodeValue(value, bufferFactory, elementType, mimeType, hints)) + .flux(); + } + else { + ResolvableType listType = ResolvableType.forClassWithGenerics(List.class, elementType); + return Flux.from(inputStream) + .collectList() + .map(list -> encodeValue(list, bufferFactory, listType, mimeType, hints)) + .flux(); + } + } + + + @Override + public DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory, + ResolvableType valueType, @Nullable MimeType mimeType, + @Nullable Map hints) { + + KSerializer serializer = serializer(valueType); + if (serializer == null) { + throw new EncodingException("Could not find KSerializer for " + valueType); + } + String string = format().encodeToString(serializer, value); + return this.charSequenceEncoder.encodeValue(string, bufferFactory, valueType, mimeType, null); + } +} diff --git a/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationSupport.java b/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationSupport.java new file mode 100644 index 0000000000..5d356c8b49 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/KotlinSerializationSupport.java @@ -0,0 +1,153 @@ +/* + * Copyright 2002-2022 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.http.codec; + +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import kotlinx.serialization.KSerializer; +import kotlinx.serialization.SerialFormat; +import kotlinx.serialization.SerializersKt; +import kotlinx.serialization.descriptors.PolymorphicKind; +import kotlinx.serialization.descriptors.SerialDescriptor; + +import org.springframework.core.ResolvableType; +import org.springframework.lang.Nullable; +import org.springframework.util.ConcurrentReferenceHashMap; +import org.springframework.util.MimeType; + +/** + * Base class providing support methods for encoding and decoding with Kotlin + * serialization. + * + * @author Sebastien Deleuze + * @author Iain Henderson + * @author Arjen Poutsma + * @since 6.0 + * @param the type of {@link SerialFormat} + */ +public abstract class KotlinSerializationSupport { + + private static final Map> serializerCache = new ConcurrentReferenceHashMap<>(); + + + private final T format; + + private final List supportedMimeTypes; + + /** + * Creates a new instance of this support class with the given format + * and supported mime types. + */ + protected KotlinSerializationSupport(T format, MimeType... supportedMimeTypes) { + this.format = format; + this.supportedMimeTypes = Arrays.asList(supportedMimeTypes); + } + + /** + * Returns the format. + */ + protected final T format() { + return this.format; + } + + /** + * Returns the supported mime types. + */ + protected final List supportedMimeTypes() { + return this.supportedMimeTypes; + } + + /** + * Indicates whether the given type can be serialized using Kotlin + * serialization. + * @param type the type to be serialized + * @param mimeType the mimetype to use (can be {@code null}) + * @return {@code true} if {@code type} can be serialized; false otherwise + */ + protected final boolean canSerialize(ResolvableType type, @Nullable MimeType mimeType) { + KSerializer serializer = serializer(type); + if (serializer == null) { + return false; + } + else { + return (supports(mimeType) && !String.class.isAssignableFrom(type.toClass()) && + !ServerSentEvent.class.isAssignableFrom(type.toClass())); + } + + } + + private boolean supports(@Nullable MimeType mimeType) { + if (mimeType == null) { + return true; + } + for (MimeType candidate : this.supportedMimeTypes) { + if (candidate.isCompatibleWith(mimeType)) { + return true; + } + } + return false; + } + + + /** + * Returns the serializer that can (de)serialize instances of the given + * type. If no serializer can be found, or if {@code resolvableType} is + * a open polymorphic + * type, {@code null} is returned. + * @param resolvableType the type to find a serializer for + * @return a resolved serializer for the given type, or {@code null} + */ + @Nullable + protected final KSerializer serializer(ResolvableType resolvableType) { + Type type = resolvableType.getType(); + KSerializer serializer = serializerCache.get(type); + if (serializer == null) { + try { + serializer = SerializersKt.serializerOrNull(type); + } + catch (IllegalArgumentException ignored) { + } + if (serializer != null) { + if (hasPolymorphism(serializer.getDescriptor(), new HashSet<>())) { + return null; + } + serializerCache.put(type, serializer); + } + } + return serializer; + } + + private static boolean hasPolymorphism(SerialDescriptor descriptor, Set alreadyProcessed) { + alreadyProcessed.add(descriptor.getSerialName()); + if (descriptor.getKind().equals(PolymorphicKind.OPEN.INSTANCE)) { + return true; + } + for (int i = 0 ; i < descriptor.getElementsCount() ; i++) { + SerialDescriptor elementDescriptor = descriptor.getElementDescriptor(i); + if (!alreadyProcessed.contains(elementDescriptor.getSerialName()) && hasPolymorphism(elementDescriptor, alreadyProcessed)) { + return true; + } + } + return false; + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/codec/cbor/KotlinSerializationCborDecoder.java b/spring-web/src/main/java/org/springframework/http/codec/cbor/KotlinSerializationCborDecoder.java new file mode 100644 index 0000000000..4e6f17d5b7 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/cbor/KotlinSerializationCborDecoder.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2022 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.http.codec.cbor; + +import kotlinx.serialization.cbor.Cbor; + +import org.springframework.http.MediaType; +import org.springframework.http.codec.KotlinSerializationBinaryDecoder; + +/** + * Decode a byte stream into CBOR and convert to Objects with + * kotlinx.serialization. + * + *

This decoder can be used to bind {@code @Serializable} Kotlin classes, + * open polymorphic serialization + * is not supported. + * It supports {@code application/cbor}. + * + *

Decoding streams is not supported yet, see + * kotlinx.serialization/issues/1073 + * related issue. + * + * @author Iain Henderson + * @since 6.0 + */ +public class KotlinSerializationCborDecoder extends KotlinSerializationBinaryDecoder { + + public KotlinSerializationCborDecoder() { + this(Cbor.Default); + } + + public KotlinSerializationCborDecoder(Cbor cbor) { + super(cbor, MediaType.APPLICATION_CBOR); + } +} diff --git a/spring-web/src/main/java/org/springframework/http/codec/cbor/KotlinSerializationCborEncoder.java b/spring-web/src/main/java/org/springframework/http/codec/cbor/KotlinSerializationCborEncoder.java new file mode 100644 index 0000000000..af20d5bb70 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/cbor/KotlinSerializationCborEncoder.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2022 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.http.codec.cbor; + +import kotlinx.serialization.cbor.Cbor; + +import org.springframework.http.MediaType; +import org.springframework.http.codec.KotlinSerializationBinaryEncoder; + +/** + * Encode from an {@code Object} stream to a byte stream of CBOR objects using + * kotlinx.serialization. + * + *

This encoder can be used to bind {@code @Serializable} Kotlin classes, + * open polymorphic serialization + * is not supported. + * It supports {@code application/cbor}. + * + * @author Iain Henderson + * @since 6.0 + */ +public class KotlinSerializationCborEncoder extends KotlinSerializationBinaryEncoder { + + public KotlinSerializationCborEncoder() { + this(Cbor.Default); + } + + public KotlinSerializationCborEncoder(Cbor cbor) { + super(cbor, MediaType.APPLICATION_CBOR); + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonDecoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonDecoder.java index 06094776e5..74e10f76eb 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonDecoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonDecoder.java @@ -16,28 +16,10 @@ package org.springframework.http.codec.json; -import java.lang.reflect.Type; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import kotlinx.serialization.KSerializer; -import kotlinx.serialization.SerializersKt; -import kotlinx.serialization.descriptors.PolymorphicKind; -import kotlinx.serialization.descriptors.SerialDescriptor; import kotlinx.serialization.json.Json; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import org.springframework.core.ResolvableType; -import org.springframework.core.codec.AbstractDecoder; -import org.springframework.core.codec.StringDecoder; -import org.springframework.core.io.buffer.DataBuffer; import org.springframework.http.MediaType; -import org.springframework.lang.Nullable; -import org.springframework.util.ConcurrentReferenceHashMap; -import org.springframework.util.MimeType; +import org.springframework.http.codec.KotlinSerializationStringDecoder; /** * Decode a byte stream into JSON and convert to Object's with @@ -54,110 +36,17 @@ import org.springframework.util.MimeType; * related issue. * * @author Sebastien Deleuze + * @author Iain Henderson * @since 5.3 */ -public class KotlinSerializationJsonDecoder extends AbstractDecoder { - - private static final Map> serializerCache = new ConcurrentReferenceHashMap<>(); - - private final Json json; - - // String decoding needed for now, see https://github.com/Kotlin/kotlinx.serialization/issues/204 for more details - private final StringDecoder stringDecoder = StringDecoder.allMimeTypes(StringDecoder.DEFAULT_DELIMITERS, false); - +public class KotlinSerializationJsonDecoder extends KotlinSerializationStringDecoder { public KotlinSerializationJsonDecoder() { this(Json.Default); } public KotlinSerializationJsonDecoder(Json json) { - super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json")); - this.json = json; - } - - /** - * Configure a limit on the number of bytes that can be buffered whenever - * the input stream needs to be aggregated. This can be a result of - * decoding to a single {@code DataBuffer}, - * {@link java.nio.ByteBuffer ByteBuffer}, {@code byte[]}, - * {@link org.springframework.core.io.Resource Resource}, {@code String}, etc. - * It can also occur when splitting the input stream, e.g. delimited text, - * in which case the limit applies to data buffered between delimiters. - *

By default this is set to 256K. - * @param byteCount the max number of bytes to buffer, or -1 for unlimited - */ - public void setMaxInMemorySize(int byteCount) { - this.stringDecoder.setMaxInMemorySize(byteCount); - } - - /** - * Return the {@link #setMaxInMemorySize configured} byte count limit. - */ - public int getMaxInMemorySize() { - return this.stringDecoder.getMaxInMemorySize(); - } - - - @Override - public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) { - return (serializer(elementType.getType()) != null && - super.canDecode(elementType, mimeType) && - !CharSequence.class.isAssignableFrom(elementType.toClass())); - } - - @Override - public Flux decode(Publisher inputStream, ResolvableType elementType, - @Nullable MimeType mimeType, @Nullable Map hints) { - - return Flux.error(new UnsupportedOperationException()); - } - - @Override - public Mono decodeToMono(Publisher inputStream, ResolvableType elementType, - @Nullable MimeType mimeType, @Nullable Map hints) { - - return this.stringDecoder - .decodeToMono(inputStream, elementType, mimeType, hints) - .map(jsonText -> this.json.decodeFromString(serializer(elementType.getType()), jsonText)); - } - - /** - * 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. - *

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 or {@code null} if no serializer - * supporting the given type can be found - */ - @Nullable - private KSerializer serializer(Type type) { - KSerializer serializer = serializerCache.get(type); - if (serializer == null) { - try { - serializer = SerializersKt.serializerOrNull(type); - } - catch (IllegalArgumentException ignored) { - } - if (serializer == null || hasPolymorphism(serializer.getDescriptor(), new HashSet<>())) { - return null; - } - serializerCache.put(type, serializer); - } - return serializer; - } - - private boolean hasPolymorphism(SerialDescriptor descriptor, Set alreadyProcessed) { - alreadyProcessed.add(descriptor.getSerialName()); - if (descriptor.getKind().equals(PolymorphicKind.OPEN.INSTANCE)) { - return true; - } - for (int i = 0 ; i < descriptor.getElementsCount() ; i++) { - SerialDescriptor elementDescriptor = descriptor.getElementDescriptor(i); - if (!alreadyProcessed.contains(elementDescriptor.getSerialName()) && hasPolymorphism(elementDescriptor, alreadyProcessed)) { - return true; - } - } - return false; + super(json, MediaType.APPLICATION_JSON, new MediaType("application", "*+json")); } } diff --git a/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonEncoder.java b/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonEncoder.java index ef5e0ac492..45f7972de5 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonEncoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/json/KotlinSerializationJsonEncoder.java @@ -16,31 +16,10 @@ package org.springframework.http.codec.json; -import java.lang.reflect.Type; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import kotlinx.serialization.KSerializer; -import kotlinx.serialization.SerializersKt; -import kotlinx.serialization.descriptors.PolymorphicKind; -import kotlinx.serialization.descriptors.SerialDescriptor; import kotlinx.serialization.json.Json; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import org.springframework.core.ResolvableType; -import org.springframework.core.codec.AbstractEncoder; -import org.springframework.core.codec.CharSequenceEncoder; -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.http.MediaType; -import org.springframework.http.codec.ServerSentEvent; -import org.springframework.lang.Nullable; -import org.springframework.util.ConcurrentReferenceHashMap; -import org.springframework.util.MimeType; +import org.springframework.http.codec.KotlinSerializationStringEncoder; /** * Encode from an {@code Object} stream to a byte stream of JSON objects using @@ -53,99 +32,17 @@ import org.springframework.util.MimeType; * various character sets, {@code UTF-8} being the default. * * @author Sebastien Deleuze + * @author Iain Henderson * @since 5.3 */ -public class KotlinSerializationJsonEncoder extends AbstractEncoder { - - private static final Map> serializerCache = new ConcurrentReferenceHashMap<>(); - - private final Json json; - - // CharSequence encoding needed for now, see https://github.com/Kotlin/kotlinx.serialization/issues/204 for more details - private final CharSequenceEncoder charSequenceEncoder = CharSequenceEncoder.allMimeTypes(); - +public class KotlinSerializationJsonEncoder extends KotlinSerializationStringEncoder { public KotlinSerializationJsonEncoder() { this(Json.Default); } public KotlinSerializationJsonEncoder(Json json) { - super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json")); - this.json = json; - } - - - @Override - public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) { - return (serializer(elementType.getType()) != null && - super.canEncode(elementType, mimeType) && - !String.class.isAssignableFrom(elementType.toClass()) && - !ServerSentEvent.class.isAssignableFrom(elementType.toClass())); - } - - @Override - public Flux encode(Publisher inputStream, DataBufferFactory bufferFactory, - ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map hints) { - - if (inputStream instanceof Mono) { - return Mono.from(inputStream) - .map(value -> encodeValue(value, bufferFactory, elementType, mimeType, hints)) - .flux(); - } - else { - ResolvableType listType = ResolvableType.forClassWithGenerics(List.class, elementType); - return Flux.from(inputStream) - .collectList() - .map(list -> encodeValue(list, bufferFactory, listType, mimeType, hints)) - .flux(); - } - } - - @Override - public DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory, - ResolvableType valueType, @Nullable MimeType mimeType, @Nullable Map hints) { - - String json = this.json.encodeToString(serializer(valueType.getType()), value); - return this.charSequenceEncoder.encodeValue(json, bufferFactory, valueType, mimeType, null); - } - - /** - * 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. - *

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 or {@code null} if no serializer - * supporting the given type can be found - */ - @Nullable - private KSerializer serializer(Type type) { - KSerializer serializer = serializerCache.get(type); - if (serializer == null) { - try { - serializer = SerializersKt.serializerOrNull(type); - } - catch (IllegalArgumentException ignored) { - } - if (serializer == null || hasPolymorphism(serializer.getDescriptor(), new HashSet<>())) { - return null; - } - serializerCache.put(type, serializer); - } - return serializer; - } - - private boolean hasPolymorphism(SerialDescriptor descriptor, Set alreadyProcessed) { - alreadyProcessed.add(descriptor.getSerialName()); - if (descriptor.getKind().equals(PolymorphicKind.OPEN.INSTANCE)) { - return true; - } - for (int i = 0 ; i < descriptor.getElementsCount() ; i++) { - SerialDescriptor elementDescriptor = descriptor.getElementDescriptor(i); - if (!alreadyProcessed.contains(elementDescriptor.getSerialName()) && hasPolymorphism(elementDescriptor, alreadyProcessed)) { - return true; - } - } - return false; + super(json, MediaType.APPLICATION_JSON, new MediaType("application", "*+json")); } } diff --git a/spring-web/src/main/java/org/springframework/http/codec/protobuf/KotlinSerializationProtobufDecoder.java b/spring-web/src/main/java/org/springframework/http/codec/protobuf/KotlinSerializationProtobufDecoder.java new file mode 100644 index 0000000000..a8ebef55a4 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/protobuf/KotlinSerializationProtobufDecoder.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2022 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.http.codec.protobuf; + +import kotlinx.serialization.protobuf.ProtoBuf; + +import org.springframework.http.codec.KotlinSerializationBinaryDecoder; + +/** + * Decode a byte stream into a protocol Buffer and convert to Objects with + * kotlinx.serialization. + * + *

This decoder can be used to bind {@code @Serializable} Kotlin classes, + * open polymorphic serialization + * is not supported. + * It supports {@code application/x-protobuf}, {@code application/octet-stream}, and {@code application/vnd.google.protobuf}. + * + *

Decoding streams is not supported yet, see + * kotlinx.serialization/issues/1073 + * related issue. + * + * @author Iain Henderson + * @since 6.0 + */ +public class KotlinSerializationProtobufDecoder extends KotlinSerializationBinaryDecoder { + + public KotlinSerializationProtobufDecoder() { + this(ProtoBuf.Default); + } + + public KotlinSerializationProtobufDecoder(ProtoBuf protobuf) { + super(protobuf, ProtobufCodecSupport.MIME_TYPES); + } +} diff --git a/spring-web/src/main/java/org/springframework/http/codec/protobuf/KotlinSerializationProtobufEncoder.java b/spring-web/src/main/java/org/springframework/http/codec/protobuf/KotlinSerializationProtobufEncoder.java new file mode 100644 index 0000000000..d9b0fc0b29 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/codec/protobuf/KotlinSerializationProtobufEncoder.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2022 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.http.codec.protobuf; + +import kotlinx.serialization.protobuf.ProtoBuf; + +import org.springframework.http.codec.KotlinSerializationBinaryEncoder; + +/** + * Decode a byte stream into a Protocol Buffer and convert to Objects with + * kotlinx.serialization. + * + *

This decoder can be used to bind {@code @Serializable} Kotlin classes, + * open polymorphic serialization + * is not supported. + * It supports {@code application/x-protobuf}, {@code application/octet-stream}, and {@code application/vnd.google.protobuf}. + * + *

Decoding streams is not supported yet, see + * kotlinx.serialization/issues/1073 + * related issue. + * + * @author Iain Henderson + * @since 6.0 + */ +public class KotlinSerializationProtobufEncoder extends KotlinSerializationBinaryEncoder { + + public KotlinSerializationProtobufEncoder() { + this(ProtoBuf.Default); + } + + public KotlinSerializationProtobufEncoder(ProtoBuf protobuf) { + super(protobuf, ProtobufCodecSupport.MIME_TYPES); + } +} diff --git a/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufCodecSupport.java b/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufCodecSupport.java index 1679b5a7c5..aef7b72cbe 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufCodecSupport.java +++ b/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufCodecSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -16,6 +16,7 @@ package org.springframework.http.codec.protobuf; +import java.util.Arrays; import java.util.List; import org.springframework.lang.Nullable; @@ -29,10 +30,11 @@ import org.springframework.util.MimeType; */ public abstract class ProtobufCodecSupport { - static final List MIME_TYPES = List.of( - new MimeType("application", "x-protobuf"), - new MimeType("application", "octet-stream"), - new MimeType("application", "vnd.google.protobuf")); + static final MimeType[] MIME_TYPES = new MimeType[]{ + new MimeType("application", "x-protobuf"), + new MimeType("application", "octet-stream"), + new MimeType("application", "vnd.google.protobuf") + }; static final String DELIMITED_KEY = "delimited"; @@ -40,11 +42,19 @@ public abstract class ProtobufCodecSupport { protected boolean supportsMimeType(@Nullable MimeType mimeType) { - return (mimeType == null || MIME_TYPES.stream().anyMatch(m -> m.isCompatibleWith(mimeType))); + if (mimeType == null) { + return true; + } + for (MimeType m : MIME_TYPES) { + if (m.isCompatibleWith(mimeType)) { + return true; + } + } + return false; } protected List getMimeTypes() { - return MIME_TYPES; + return Arrays.asList(MIME_TYPES); } } diff --git a/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufEncoder.java b/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufEncoder.java index 43469fb0c8..39a5c94a68 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufEncoder.java +++ b/spring-web/src/main/java/org/springframework/http/codec/protobuf/ProtobufEncoder.java @@ -17,6 +17,7 @@ package org.springframework.http.codec.protobuf; import java.io.IOException; +import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; @@ -56,8 +57,7 @@ import org.springframework.util.MimeType; */ public class ProtobufEncoder extends ProtobufCodecSupport implements HttpMessageEncoder { - private static final List streamingMediaTypes = MIME_TYPES - .stream() + private static final List streamingMediaTypes = Arrays.stream(MIME_TYPES) .map(mimeType -> new MediaType(mimeType.getType(), mimeType.getSubtype(), Collections.singletonMap(DELIMITED_KEY, DELIMITED_VALUE))) .toList(); diff --git a/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java b/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java index 6f7337d87d..d5d77ce726 100644 --- a/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java +++ b/spring-web/src/main/java/org/springframework/http/codec/support/BaseDefaultCodecs.java @@ -50,6 +50,8 @@ import org.springframework.http.codec.ResourceHttpMessageReader; import org.springframework.http.codec.ResourceHttpMessageWriter; import org.springframework.http.codec.ServerSentEventHttpMessageReader; import org.springframework.http.codec.ServerSentEventHttpMessageWriter; +import org.springframework.http.codec.cbor.KotlinSerializationCborDecoder; +import org.springframework.http.codec.cbor.KotlinSerializationCborEncoder; import org.springframework.http.codec.json.AbstractJackson2Decoder; import org.springframework.http.codec.json.Jackson2JsonDecoder; import org.springframework.http.codec.json.Jackson2JsonEncoder; @@ -61,6 +63,8 @@ import org.springframework.http.codec.multipart.DefaultPartHttpMessageReader; import org.springframework.http.codec.multipart.MultipartHttpMessageReader; import org.springframework.http.codec.multipart.MultipartHttpMessageWriter; import org.springframework.http.codec.multipart.PartEventHttpMessageReader; +import org.springframework.http.codec.protobuf.KotlinSerializationProtobufDecoder; +import org.springframework.http.codec.protobuf.KotlinSerializationProtobufEncoder; import org.springframework.http.codec.protobuf.ProtobufDecoder; import org.springframework.http.codec.protobuf.ProtobufEncoder; import org.springframework.http.codec.protobuf.ProtobufHttpMessageWriter; @@ -100,8 +104,12 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure static final boolean netty5BufferPresent; + static final boolean kotlinSerializationCborPresent; + static final boolean kotlinSerializationJsonPresent; + static final boolean kotlinSerializationProtobufPresent; + static { ClassLoader classLoader = BaseCodecConfigurer.class.getClassLoader(); jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) && @@ -112,7 +120,9 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure synchronossMultipartPresent = ClassUtils.isPresent("org.synchronoss.cloud.nio.multipart.NioMultipartParser", classLoader); nettyByteBufPresent = ClassUtils.isPresent("io.netty.buffer.ByteBuf", classLoader); netty5BufferPresent = ClassUtils.isPresent("io.netty5.buffer.Buffer", classLoader); + kotlinSerializationCborPresent = ClassUtils.isPresent("kotlinx.serialization.cbor.Cbor", classLoader); kotlinSerializationJsonPresent = ClassUtils.isPresent("kotlinx.serialization.json.Json", classLoader); + kotlinSerializationProtobufPresent = ClassUtils.isPresent("kotlinx.serialization.protobuf.ProtoBuf", classLoader); } @@ -140,12 +150,24 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure @Nullable private Encoder jaxb2Encoder; + @Nullable + private Decoder kotlinSerializationCborDecoder; + + @Nullable + private Encoder kotlinSerializationCborEncoder; + @Nullable private Decoder kotlinSerializationJsonDecoder; @Nullable private Encoder kotlinSerializationJsonEncoder; + @Nullable + private Decoder kotlinSerializationProtobufDecoder; + + @Nullable + private Encoder kotlinSerializationProtobufEncoder; + @Nullable private Consumer codecConsumer; @@ -204,8 +226,12 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure this.protobufEncoder = other.protobufEncoder; this.jaxb2Decoder = other.jaxb2Decoder; this.jaxb2Encoder = other.jaxb2Encoder; + this.kotlinSerializationCborDecoder = other.kotlinSerializationCborDecoder; + this.kotlinSerializationCborEncoder = other.kotlinSerializationCborEncoder; this.kotlinSerializationJsonDecoder = other.kotlinSerializationJsonDecoder; this.kotlinSerializationJsonEncoder = other.kotlinSerializationJsonEncoder; + this.kotlinSerializationProtobufDecoder = other.kotlinSerializationProtobufDecoder; + this.kotlinSerializationProtobufEncoder = other.kotlinSerializationProtobufEncoder; this.codecConsumer = other.codecConsumer; this.maxInMemorySize = other.maxInMemorySize; this.enableLoggingRequestDetails = other.enableLoggingRequestDetails; @@ -266,6 +292,18 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure initObjectWriters(); } + @Override + public void kotlinSerializationCborDecoder(Decoder decoder) { + this.kotlinSerializationCborDecoder = decoder; + initObjectReaders(); + } + + @Override + public void kotlinSerializationCborEncoder(Encoder encoder) { + this.kotlinSerializationCborEncoder = encoder; + initObjectWriters(); + } + @Override public void kotlinSerializationJsonDecoder(Decoder decoder) { this.kotlinSerializationJsonDecoder = decoder; @@ -278,6 +316,18 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure initObjectWriters(); } + @Override + public void kotlinSerializationProtobufDecoder(Decoder decoder) { + this.kotlinSerializationProtobufDecoder = decoder; + initObjectReaders(); + } + + @Override + public void kotlinSerializationProtobufEncoder(Encoder encoder) { + this.kotlinSerializationProtobufEncoder = encoder; + initObjectWriters(); + } + @Override public void configureDefaultCodec(Consumer codecConsumer) { this.codecConsumer = (this.codecConsumer != null ? @@ -358,6 +408,10 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure addCodec(this.typedReaders, new DecoderHttpMessageReader<>(this.protobufDecoder != null ? (ProtobufDecoder) this.protobufDecoder : new ProtobufDecoder())); } + else if (kotlinSerializationProtobufPresent) { + addCodec(this.typedReaders, new DecoderHttpMessageReader<>(this.kotlinSerializationProtobufDecoder != null ? + (KotlinSerializationProtobufDecoder) this.kotlinSerializationProtobufDecoder : new KotlinSerializationProtobufDecoder())); + } addCodec(this.typedReaders, new FormHttpMessageReader()); // client vs server.. @@ -401,11 +455,21 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure ((ProtobufDecoder) codec).setMaxMessageSize(size); } } + if (kotlinSerializationCborPresent) { + if (codec instanceof KotlinSerializationCborDecoder) { + ((KotlinSerializationCborDecoder) codec).setMaxInMemorySize(size); + } + } if (kotlinSerializationJsonPresent) { if (codec instanceof KotlinSerializationJsonDecoder) { ((KotlinSerializationJsonDecoder) codec).setMaxInMemorySize(size); } } + if (kotlinSerializationProtobufPresent) { + if (codec instanceof KotlinSerializationProtobufDecoder) { + ((KotlinSerializationProtobufDecoder) codec).setMaxInMemorySize(size); + } + } if (jackson2Present) { if (codec instanceof AbstractJackson2Decoder) { ((AbstractJackson2Decoder) codec).setMaxInMemorySize(size); @@ -493,9 +557,20 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure if (!this.registerDefaults) { return; } + if (kotlinSerializationCborPresent) { + addCodec(this.objectReaders, new DecoderHttpMessageReader<>(this.kotlinSerializationCborDecoder != null ? + (KotlinSerializationCborDecoder) this.kotlinSerializationCborDecoder : + new KotlinSerializationCborDecoder())); + } if (kotlinSerializationJsonPresent) { addCodec(this.objectReaders, new DecoderHttpMessageReader<>(getKotlinSerializationJsonDecoder())); } + if (kotlinSerializationProtobufPresent) { + addCodec(this.objectReaders, + new DecoderHttpMessageReader<>(this.kotlinSerializationProtobufDecoder != null ? + (KotlinSerializationProtobufDecoder) this.kotlinSerializationProtobufDecoder : + new KotlinSerializationProtobufDecoder())); + } if (jackson2Present) { addCodec(this.objectReaders, new DecoderHttpMessageReader<>(getJackson2JsonDecoder())); } @@ -533,7 +608,6 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure /** * Return all writers that support specific types. */ - @SuppressWarnings({"rawtypes" }) final List> getTypedWriters() { return this.typedWriters; } @@ -608,9 +682,19 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure */ final List> getBaseObjectWriters() { List> writers = new ArrayList<>(); + if (kotlinSerializationCborPresent) { + addCodec(writers, new EncoderHttpMessageWriter<>(this.kotlinSerializationCborEncoder != null ? + (KotlinSerializationCborEncoder) this.kotlinSerializationCborEncoder : + new KotlinSerializationCborEncoder())); + } if (kotlinSerializationJsonPresent) { addCodec(writers, new EncoderHttpMessageWriter<>(getKotlinSerializationJsonEncoder())); } + if (kotlinSerializationProtobufPresent) { + addCodec(writers, new EncoderHttpMessageWriter<>(this.kotlinSerializationProtobufEncoder != null ? + (KotlinSerializationProtobufEncoder) this.kotlinSerializationProtobufEncoder : + new KotlinSerializationProtobufEncoder())); + } if (jackson2Present) { addCodec(writers, new EncoderHttpMessageWriter<>(getJackson2JsonEncoder())); } diff --git a/spring-web/src/main/java/org/springframework/http/converter/AbstractKotlinSerializationHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/AbstractKotlinSerializationHttpMessageConverter.java new file mode 100644 index 0000000000..8f047499e0 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/converter/AbstractKotlinSerializationHttpMessageConverter.java @@ -0,0 +1,182 @@ +/* + * Copyright 2002-2022 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.http.converter; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import kotlinx.serialization.KSerializer; +import kotlinx.serialization.SerialFormat; +import kotlinx.serialization.SerializersKt; +import kotlinx.serialization.descriptors.PolymorphicKind; +import kotlinx.serialization.descriptors.SerialDescriptor; + +import org.springframework.core.GenericTypeResolver; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.lang.Nullable; +import org.springframework.util.ConcurrentReferenceHashMap; + + +/** + * Abstract base class for {@link HttpMessageConverter} implementations that + * use Kotlin serialization. + * + * @author Andreas Ahlenstorf + * @author Sebastien Deleuze + * @author Juergen Hoeller + * @author Iain Henderson + * @author Arjen Poutsma + * @since 6.0 + * @param the type of {@link SerialFormat} + */ +public abstract class AbstractKotlinSerializationHttpMessageConverter extends AbstractGenericHttpMessageConverter { + + private static final Map> serializerCache = new ConcurrentReferenceHashMap<>(); + + + private final T format; + + + /** + * Construct an {@code AbstractKotlinSerializationHttpMessageConverter} with multiple supported media type and + * format. + * @param format the format + * @param supportedMediaTypes the supported media types + */ + protected AbstractKotlinSerializationHttpMessageConverter(T format, MediaType... supportedMediaTypes) { + super(supportedMediaTypes); + this.format = format; + } + + + @Override + protected boolean supports(Class clazz) { + return serializer(clazz) != null; + } + + @Override + public boolean canRead(Type type, @Nullable Class contextClass, @Nullable MediaType mediaType) { + if (serializer(GenericTypeResolver.resolveType(type, contextClass)) != null) { + return canRead(mediaType); + } + else { + return false; + } + } + + @Override + public boolean canWrite(@Nullable Type type, Class clazz, @Nullable MediaType mediaType) { + if (serializer(type != null ? GenericTypeResolver.resolveType(type, clazz) : clazz) != null) { + return canWrite(mediaType); + } + else { + return false; + } + } + + @Override + public final Object read(Type type, @Nullable Class contextClass, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException { + + Type resolvedType = GenericTypeResolver.resolveType(type, contextClass); + KSerializer serializer = serializer(resolvedType); + if (serializer == null) { + throw new HttpMessageNotReadableException("Could not find KSerializer for " + resolvedType, inputMessage); + } + return readInternal(serializer, this.format, inputMessage); + } + + @Override + protected final Object readInternal(Class clazz, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException { + + KSerializer serializer = serializer(clazz); + if (serializer == null) { + throw new HttpMessageNotReadableException("Could not find KSerializer for " + clazz, inputMessage); + } + return readInternal(serializer, this.format, inputMessage); + } + + /** + * Reads the given input message with the given serializer and format. + */ + protected abstract Object readInternal(KSerializer serializer, T format, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException; + + @Override + protected final void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException { + + Type resolvedType = type != null ? type : object.getClass(); + KSerializer serializer = serializer(resolvedType); + if (serializer == null) { + throw new HttpMessageNotWritableException("Could not find KSerializer for " + resolvedType); + } + writeInternal(object, serializer, this.format, outputMessage); + } + + /** + * Write the given object to the output message with the given serializer and format. + */ + protected abstract void writeInternal(Object object, KSerializer serializer, T format, + HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException; + + /** + * Tries to find a serializer that can marshall or unmarshall instances of the given type + * using kotlinx.serialization. If no serializer can be found, {@code null} is returned. + *

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, or {@code null} + */ + @Nullable + private KSerializer serializer(Type type) { + KSerializer serializer = serializerCache.get(type); + if (serializer == null) { + try { + serializer = SerializersKt.serializerOrNull(type); + } + catch (IllegalArgumentException ignored) { + } + if (serializer != null) { + if (hasPolymorphism(serializer.getDescriptor(), new HashSet<>())) { + return null; + } + serializerCache.put(type, serializer); + } + } + return serializer; + } + + private boolean hasPolymorphism(SerialDescriptor descriptor, Set alreadyProcessed) { + alreadyProcessed.add(descriptor.getSerialName()); + if (descriptor.getKind().equals(PolymorphicKind.OPEN.INSTANCE)) { + return true; + } + for (int i = 0 ; i < descriptor.getElementsCount() ; i++) { + SerialDescriptor elementDescriptor = descriptor.getElementDescriptor(i); + if (!alreadyProcessed.contains(elementDescriptor.getSerialName()) && hasPolymorphism(elementDescriptor, alreadyProcessed)) { + return true; + } + } + return false; + } +} diff --git a/spring-web/src/main/java/org/springframework/http/converter/KotlinSerializationBinaryHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/KotlinSerializationBinaryHttpMessageConverter.java new file mode 100644 index 0000000000..bc094e9fe4 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/converter/KotlinSerializationBinaryHttpMessageConverter.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2022 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.http.converter; + +import java.io.IOException; + +import kotlinx.serialization.BinaryFormat; +import kotlinx.serialization.KSerializer; +import kotlinx.serialization.SerializationException; + +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.util.StreamUtils; + +/** + * Abstract base class for {@link HttpMessageConverter} implementations that + * defer to Kotlin {@linkplain BinaryFormat binary serializers}. + * + * @author Andreas Ahlenstorf + * @author Sebastien Deleuze + * @author Juergen Hoeller + * @author Iain Henderson + * @author Arjen Poutsma + * @since 6.0 + * @param the type of {@link BinaryFormat} + */ +public abstract class KotlinSerializationBinaryHttpMessageConverter + extends AbstractKotlinSerializationHttpMessageConverter { + + /** + * Construct an {@code KotlinSerializationBinaryHttpMessageConverter} with format and supported media types. + */ + protected KotlinSerializationBinaryHttpMessageConverter(T format, MediaType... supportedMediaTypes) { + super(format, supportedMediaTypes); + } + + @Override + protected Object readInternal(KSerializer serializer, T format, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException { + + byte[] bytes = StreamUtils.copyToByteArray(inputMessage.getBody()); + try { + return format.decodeFromByteArray(serializer, bytes); + } + catch (SerializationException ex) { + throw new HttpMessageNotReadableException("Could not read " + format + ": " + ex.getMessage(), ex, + inputMessage); + } + } + + @Override + protected void writeInternal(Object object, KSerializer serializer, T format, + HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { + + try { + byte[] bytes = format.encodeToByteArray(serializer, object); + outputMessage.getBody().write(bytes); + outputMessage.getBody().flush(); + } + catch (SerializationException ex) { + throw new HttpMessageNotWritableException("Could not write " + format + ": " + ex.getMessage(), ex); + } + } +} diff --git a/spring-web/src/main/java/org/springframework/http/converter/KotlinSerializationStringHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/KotlinSerializationStringHttpMessageConverter.java new file mode 100644 index 0000000000..f51f81ba67 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/converter/KotlinSerializationStringHttpMessageConverter.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-2022 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.http.converter; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; + +import kotlinx.serialization.KSerializer; +import kotlinx.serialization.SerializationException; +import kotlinx.serialization.StringFormat; + +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.lang.Nullable; +import org.springframework.util.StreamUtils; + +/** + * Abstract base class for {@link HttpMessageConverter} implementations that + * defer to Kotlin {@linkplain StringFormat string serializers}. + * + * @author Andreas Ahlenstorf + * @author Sebastien Deleuze + * @author Juergen Hoeller + * @author Iain Henderson + * @author Arjen Poutsma + * @since 6.0 + * @param the type of {@link StringFormat} + */ +public abstract class KotlinSerializationStringHttpMessageConverter + extends AbstractKotlinSerializationHttpMessageConverter { + + + /** + * Construct an {@code KotlinSerializationStringHttpMessageConverter} with format and supported media types. + */ + protected KotlinSerializationStringHttpMessageConverter(T format, MediaType... supportedMediaTypes) { + super(format, supportedMediaTypes); + } + + + @Override + protected Object readInternal(KSerializer serializer, T format, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException { + + Charset charset = charset(inputMessage.getHeaders().getContentType()); + String s = StreamUtils.copyToString(inputMessage.getBody(), charset); + try { + return format.decodeFromString(serializer, s); + } + catch (SerializationException ex) { + throw new HttpMessageNotReadableException("Could not read " + format + ": " + ex.getMessage(), ex, + inputMessage); + } + } + + @Override + protected void writeInternal(Object object, KSerializer serializer, T format, + HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { + + try { + String s = format.encodeToString(serializer, object); + Charset charset = charset(outputMessage.getHeaders().getContentType()); + outputMessage.getBody().write(s.getBytes(charset)); + outputMessage.getBody().flush(); + } + catch (SerializationException ex) { + throw new HttpMessageNotWritableException("Could not write " + format + ": " + ex.getMessage(), ex); + } + } + + private static Charset charset(@Nullable MediaType contentType) { + if (contentType != null && contentType.getCharset() != null) { + return contentType.getCharset(); + } + return StandardCharsets.UTF_8; + } +} diff --git a/spring-web/src/main/java/org/springframework/http/converter/cbor/KotlinSerializationCborHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/cbor/KotlinSerializationCborHttpMessageConverter.java new file mode 100644 index 0000000000..544070128b --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/converter/cbor/KotlinSerializationCborHttpMessageConverter.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2022 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.http.converter.cbor; + +import kotlinx.serialization.cbor.Cbor; + +import org.springframework.http.MediaType; +import org.springframework.http.converter.KotlinSerializationBinaryHttpMessageConverter; + +/** + * Implementation of {@link org.springframework.http.converter.HttpMessageConverter} + * that can read and write CBOR using + * kotlinx.serialization. + * + *

This converter can be used to bind {@code @Serializable} Kotlin classes, + * open polymorphic serialization + * is not supported. + * It supports {@code application/cbor}. + * + * @author Iain Henderson + * @since 6.0 + */ +public class KotlinSerializationCborHttpMessageConverter extends KotlinSerializationBinaryHttpMessageConverter { + public KotlinSerializationCborHttpMessageConverter() { + this(Cbor.Default); + } + + public KotlinSerializationCborHttpMessageConverter(Cbor cbor) { + super(cbor, MediaType.APPLICATION_CBOR); + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverter.java index 39ad1484a9..bb47542d60 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverter.java @@ -16,32 +16,10 @@ package org.springframework.http.converter.json; -import java.io.IOException; -import java.lang.reflect.Type; -import java.nio.charset.Charset; -import java.nio.charset.StandardCharsets; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import kotlinx.serialization.KSerializer; -import kotlinx.serialization.SerializationException; -import kotlinx.serialization.SerializersKt; -import kotlinx.serialization.descriptors.PolymorphicKind; -import kotlinx.serialization.descriptors.SerialDescriptor; import kotlinx.serialization.json.Json; -import org.springframework.core.GenericTypeResolver; -import org.springframework.http.HttpInputMessage; -import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; -import org.springframework.http.converter.AbstractGenericHttpMessageConverter; -import org.springframework.http.converter.HttpMessageNotReadableException; -import org.springframework.http.converter.HttpMessageNotWritableException; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; -import org.springframework.util.ConcurrentReferenceHashMap; -import org.springframework.util.StreamUtils; +import org.springframework.http.converter.KotlinSerializationStringHttpMessageConverter; /** * Implementation of {@link org.springframework.http.converter.HttpMessageConverter} @@ -57,16 +35,10 @@ import org.springframework.util.StreamUtils; * @author Andreas Ahlenstorf * @author Sebastien Deleuze * @author Juergen Hoeller + * @author Iain Henderson * @since 5.3 */ -public class KotlinSerializationJsonHttpMessageConverter extends AbstractGenericHttpMessageConverter { - - private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; - - private static final Map> serializerCache = new ConcurrentReferenceHashMap<>(); - - private final Json json; - +public class KotlinSerializationJsonHttpMessageConverter extends KotlinSerializationStringHttpMessageConverter { /** * Construct a new {@code KotlinSerializationJsonHttpMessageConverter} with the default configuration. @@ -79,125 +51,6 @@ public class KotlinSerializationJsonHttpMessageConverter extends AbstractGeneric * Construct a new {@code KotlinSerializationJsonHttpMessageConverter} with a custom configuration. */ public KotlinSerializationJsonHttpMessageConverter(Json json) { - super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json")); - this.json = json; + super(json, MediaType.APPLICATION_JSON, new MediaType("application", "*+json")); } - - - @Override - protected boolean supports(Class clazz) { - return serializer(clazz) != null; - } - - @Override - public boolean canRead(Type type, @Nullable Class contextClass, @Nullable MediaType mediaType) { - return serializer(GenericTypeResolver.resolveType(type, contextClass)) != null && canRead(mediaType); - } - - @Override - public boolean canWrite(@Nullable Type type, Class clazz, @Nullable MediaType mediaType) { - return serializer(type != null ? GenericTypeResolver.resolveType(type, clazz) : clazz) != null && canWrite(mediaType); - } - - @Override - public final Object read(Type type, @Nullable Class contextClass, HttpInputMessage inputMessage) - throws IOException, HttpMessageNotReadableException { - KSerializer serializer = serializer(GenericTypeResolver.resolveType(type, contextClass)); - Assert.notNull(serializer, "The serializer should not be null"); - return decode(serializer, inputMessage); - } - - @Override - protected final Object readInternal(Class clazz, HttpInputMessage inputMessage) - throws IOException, HttpMessageNotReadableException { - KSerializer serializer = serializer(clazz); - Assert.notNull(serializer, "The serializer should not be null"); - return decode(serializer, inputMessage); - } - - private Object decode(KSerializer serializer, HttpInputMessage inputMessage) - throws IOException, HttpMessageNotReadableException { - - MediaType contentType = inputMessage.getHeaders().getContentType(); - String jsonText = StreamUtils.copyToString(inputMessage.getBody(), getCharsetToUse(contentType)); - try { - // TODO Use stream based API when available - return this.json.decodeFromString(serializer, jsonText); - } - catch (SerializationException ex) { - throw new HttpMessageNotReadableException("Could not read JSON: " + ex.getMessage(), ex, inputMessage); - } - } - - @Override - protected final void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage) - throws IOException, HttpMessageNotWritableException { - KSerializer serializer = serializer(type != null ? type : object.getClass()); - Assert.notNull(serializer, "The serializer should not be null"); - encode(object, serializer, outputMessage); - } - - private void encode(Object object, KSerializer serializer, HttpOutputMessage outputMessage) - throws IOException, HttpMessageNotWritableException { - - try { - String json = this.json.encodeToString(serializer, object); - MediaType contentType = outputMessage.getHeaders().getContentType(); - outputMessage.getBody().write(json.getBytes(getCharsetToUse(contentType))); - outputMessage.getBody().flush(); - } - catch (IOException ex) { - throw ex; - } - catch (Exception ex) { - throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex); - } - } - - private Charset getCharsetToUse(@Nullable MediaType contentType) { - if (contentType != null && contentType.getCharset() != null) { - return contentType.getCharset(); - } - return DEFAULT_CHARSET; - } - - /** - * 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. - *

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 or {@code null} if no serializer - * supporting the given type can be found - */ - @Nullable - private KSerializer serializer(Type type) { - KSerializer serializer = serializerCache.get(type); - if (serializer == null) { - try { - serializer = SerializersKt.serializerOrNull(type); - } - catch (IllegalArgumentException ignored) { - } - if (serializer == null || hasPolymorphism(serializer.getDescriptor(), new HashSet<>())) { - return null; - } - serializerCache.put(type, serializer); - } - return serializer; - } - - private boolean hasPolymorphism(SerialDescriptor descriptor, Set alreadyProcessed) { - alreadyProcessed.add(descriptor.getSerialName()); - if (descriptor.getKind().equals(PolymorphicKind.OPEN.INSTANCE)) { - return true; - } - for (int i = 0 ; i < descriptor.getElementsCount() ; i++) { - SerialDescriptor elementDescriptor = descriptor.getElementDescriptor(i); - if (!alreadyProcessed.contains(elementDescriptor.getSerialName()) && hasPolymorphism(elementDescriptor, alreadyProcessed)) { - return true; - } - } - return false; - } - } diff --git a/spring-web/src/main/java/org/springframework/http/converter/protobuf/KotlinSerializationProtobufHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/protobuf/KotlinSerializationProtobufHttpMessageConverter.java new file mode 100644 index 0000000000..44448fb774 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/converter/protobuf/KotlinSerializationProtobufHttpMessageConverter.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2022 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.http.converter.protobuf; + +import kotlinx.serialization.protobuf.ProtoBuf; + +import org.springframework.http.MediaType; +import org.springframework.http.converter.KotlinSerializationBinaryHttpMessageConverter; + +/** + * Implementation of {@link org.springframework.http.converter.HttpMessageConverter} + * that can read and write Protocol Buffers using + * kotlinx.serialization. + * + *

This converter can be used to bind {@code @Serializable} Kotlin classes, + * open polymorphic serialization + * is not supported. + * It supports {@code application/x-protobuf}, {@code application/octet-stream}, and {@code application/vnd.google.protobuf}. + * + * @author Iain Henderson + * @since 6.0 + */ +public class KotlinSerializationProtobufHttpMessageConverter extends + KotlinSerializationBinaryHttpMessageConverter { + + public KotlinSerializationProtobufHttpMessageConverter() { + this(ProtoBuf.Default); + } + + public KotlinSerializationProtobufHttpMessageConverter(ProtoBuf protobuf) { + super(protobuf, MediaType.APPLICATION_PROTOBUF, MediaType.APPLICATION_OCTET_STREAM, + new MediaType("application", "vnd.google.protobuf")); + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java index 6fd6aa6e49..1491e83108 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2021 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. @@ -18,10 +18,12 @@ package org.springframework.http.converter.support; import org.springframework.core.SpringProperties; import org.springframework.http.converter.FormHttpMessageConverter; +import org.springframework.http.converter.cbor.KotlinSerializationCborHttpMessageConverter; import org.springframework.http.converter.json.GsonHttpMessageConverter; import org.springframework.http.converter.json.JsonbHttpMessageConverter; import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.http.converter.protobuf.KotlinSerializationProtobufHttpMessageConverter; import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter; import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; @@ -58,8 +60,12 @@ public class AllEncompassingFormHttpMessageConverter extends FormHttpMessageConv private static final boolean jsonbPresent; + private static final boolean kotlinSerializationCborPresent; + private static final boolean kotlinSerializationJsonPresent; + private static final boolean kotlinSerializationProtobufPresent; + static { ClassLoader classLoader = AllEncompassingFormHttpMessageConverter.class.getClassLoader(); jaxb2Present = ClassUtils.isPresent("jakarta.xml.bind.Binder", classLoader); @@ -69,7 +75,9 @@ public class AllEncompassingFormHttpMessageConverter extends FormHttpMessageConv jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader); gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader); jsonbPresent = ClassUtils.isPresent("jakarta.json.bind.Jsonb", classLoader); + kotlinSerializationCborPresent = ClassUtils.isPresent("kotlinx.serialization.cbor.Cbor", classLoader); kotlinSerializationJsonPresent = ClassUtils.isPresent("kotlinx.serialization.json.Json", classLoader); + kotlinSerializationProtobufPresent = ClassUtils.isPresent("kotlinx.serialization.protobuf.ProtoBuf", classLoader); } @@ -107,6 +115,14 @@ public class AllEncompassingFormHttpMessageConverter extends FormHttpMessageConv if (jackson2SmilePresent) { addPartConverter(new MappingJackson2SmileHttpMessageConverter()); } + + if (kotlinSerializationCborPresent) { + addPartConverter(new KotlinSerializationCborHttpMessageConverter()); + } + + if (kotlinSerializationProtobufPresent) { + addPartConverter(new KotlinSerializationProtobufHttpMessageConverter()); + } } } diff --git a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java index be5997cf9b..edb93690c9 100644 --- a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java +++ b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java @@ -54,6 +54,7 @@ import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.ResourceHttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.converter.cbor.KotlinSerializationCborHttpMessageConverter; import org.springframework.http.converter.cbor.MappingJackson2CborHttpMessageConverter; import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter; import org.springframework.http.converter.feed.RssChannelHttpMessageConverter; @@ -61,6 +62,7 @@ import org.springframework.http.converter.json.GsonHttpMessageConverter; import org.springframework.http.converter.json.JsonbHttpMessageConverter; import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.http.converter.protobuf.KotlinSerializationProtobufHttpMessageConverter; import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter; import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; @@ -125,8 +127,12 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat private static final boolean jsonbPresent; + private static final boolean kotlinSerializationCborPresent; + private static final boolean kotlinSerializationJsonPresent; + private static final boolean kotlinSerializationProtobufPresent; + private static final ClientHttpObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultClientHttpObservationConvention(); static { @@ -140,7 +146,9 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader); gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader); jsonbPresent = ClassUtils.isPresent("jakarta.json.bind.Jsonb", classLoader); + kotlinSerializationCborPresent = ClassUtils.isPresent("kotlinx.serialization.cbor.Cbor", classLoader); kotlinSerializationJsonPresent = ClassUtils.isPresent("kotlinx.serialization.json.Json", classLoader); + kotlinSerializationProtobufPresent = ClassUtils.isPresent("kotlinx.serialization.protobuf.ProtoBuf", classLoader); } @@ -190,6 +198,10 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat } } + if (kotlinSerializationProtobufPresent) { + this.messageConverters.add(new KotlinSerializationProtobufHttpMessageConverter()); + } + if (kotlinSerializationJsonPresent) { this.messageConverters.add(new KotlinSerializationJsonHttpMessageConverter()); } @@ -206,9 +218,13 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat if (jackson2SmilePresent) { this.messageConverters.add(new MappingJackson2SmileHttpMessageConverter()); } + if (jackson2CborPresent) { this.messageConverters.add(new MappingJackson2CborHttpMessageConverter()); } + else if (kotlinSerializationCborPresent) { + this.messageConverters.add(new KotlinSerializationCborHttpMessageConverter()); + } updateErrorHandlerConverters(); this.uriTemplateHandler = initUriTemplateHandler(); diff --git a/spring-web/src/test/java/org/springframework/http/codec/CancelWithoutDemandCodecTests.java b/spring-web/src/test/java/org/springframework/http/codec/CancelWithoutDemandCodecTests.java index 8fad5f68ed..533bfe0e78 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/CancelWithoutDemandCodecTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/CancelWithoutDemandCodecTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2022 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. @@ -107,7 +107,7 @@ public class CancelWithoutDemandCodecTests { Flux flux = encoder.encode(Mono.just(msg), this.bufferFactory, ResolvableType.forClass(Msg.class), - new MimeType("application", "x-protobuf"), Collections.emptyMap()); + MediaType.APPLICATION_PROTOBUF, Collections.emptyMap()); BaseSubscriber subscriber = new ZeroDemandSubscriber(); flux.subscribe(subscriber); // Assume sync execution (e.g. encoding with Flux.just).. diff --git a/spring-web/src/test/java/org/springframework/http/codec/protobuf/ProtobufDecoderTests.java b/spring-web/src/test/java/org/springframework/http/codec/protobuf/ProtobufDecoderTests.java index 6f239dea7f..81b26e4f7d 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/protobuf/ProtobufDecoderTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/protobuf/ProtobufDecoderTests.java @@ -33,7 +33,6 @@ import org.springframework.core.testfixture.codec.AbstractDecoderTests; import org.springframework.http.MediaType; import org.springframework.protobuf.Msg; import org.springframework.protobuf.SecondMsg; -import org.springframework.util.MimeType; import static java.util.Collections.emptyMap; import static org.assertj.core.api.Assertions.assertThat; @@ -48,8 +47,6 @@ import static org.springframework.core.io.buffer.DataBufferUtils.release; */ public class ProtobufDecoderTests extends AbstractDecoderTests { - private final static MimeType PROTOBUF_MIME_TYPE = new MimeType("application", "x-protobuf"); - private final SecondMsg secondMsg = SecondMsg.newBuilder().setBlah(123).build(); private final Msg testMsg1 = Msg.newBuilder().setFoo("Foo").setBlah(secondMsg).build(); @@ -72,10 +69,10 @@ public class ProtobufDecoderTests extends AbstractDecoderTests @Override public void canDecode() { assertThat(this.decoder.canDecode(forClass(Msg.class), null)).isTrue(); - assertThat(this.decoder.canDecode(forClass(Msg.class), PROTOBUF_MIME_TYPE)).isTrue(); + assertThat(this.decoder.canDecode(forClass(Msg.class), MediaType.APPLICATION_PROTOBUF)).isTrue(); assertThat(this.decoder.canDecode(forClass(Msg.class), MediaType.APPLICATION_OCTET_STREAM)).isTrue(); assertThat(this.decoder.canDecode(forClass(Msg.class), MediaType.APPLICATION_JSON)).isFalse(); - assertThat(this.decoder.canDecode(forClass(Object.class), PROTOBUF_MIME_TYPE)).isFalse(); + assertThat(this.decoder.canDecode(forClass(Object.class), MediaType.APPLICATION_PROTOBUF)).isFalse(); } @Test diff --git a/spring-web/src/test/java/org/springframework/http/codec/support/ClientCodecConfigurerTests.java b/spring-web/src/test/java/org/springframework/http/codec/support/ClientCodecConfigurerTests.java index 80986d780f..9297a77c32 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/support/ClientCodecConfigurerTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/support/ClientCodecConfigurerTests.java @@ -55,6 +55,8 @@ import org.springframework.http.codec.HttpMessageWriter; import org.springframework.http.codec.ResourceHttpMessageReader; import org.springframework.http.codec.ResourceHttpMessageWriter; import org.springframework.http.codec.ServerSentEventHttpMessageReader; +import org.springframework.http.codec.cbor.KotlinSerializationCborDecoder; +import org.springframework.http.codec.cbor.KotlinSerializationCborEncoder; import org.springframework.http.codec.json.Jackson2CodecSupport; import org.springframework.http.codec.json.Jackson2JsonDecoder; import org.springframework.http.codec.json.Jackson2JsonEncoder; @@ -64,6 +66,8 @@ import org.springframework.http.codec.json.KotlinSerializationJsonDecoder; import org.springframework.http.codec.json.KotlinSerializationJsonEncoder; import org.springframework.http.codec.multipart.MultipartHttpMessageWriter; import org.springframework.http.codec.multipart.PartEventHttpMessageWriter; +import org.springframework.http.codec.protobuf.KotlinSerializationProtobufDecoder; +import org.springframework.http.codec.protobuf.KotlinSerializationProtobufEncoder; import org.springframework.http.codec.protobuf.ProtobufDecoder; import org.springframework.http.codec.protobuf.ProtobufHttpMessageWriter; import org.springframework.http.codec.xml.Jaxb2XmlDecoder; @@ -88,7 +92,7 @@ public class ClientCodecConfigurerTests { @Test public void defaultReaders() { List> readers = this.configurer.getReaders(); - assertThat(readers.size()).isEqualTo(15); + assertThat(readers.size()).isEqualTo(17); assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteArrayDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteBufferDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(DataBufferDecoder.class); @@ -99,7 +103,9 @@ public class ClientCodecConfigurerTests { assertThat(getNextDecoder(readers).getClass()).isEqualTo(ProtobufDecoder.class); // SPR-16804 assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(FormHttpMessageReader.class); + assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationCborDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationJsonDecoder.class); + assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationProtobufDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2JsonDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2SmileDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jaxb2XmlDecoder.class); @@ -110,7 +116,7 @@ public class ClientCodecConfigurerTests { @Test public void defaultWriters() { List> writers = this.configurer.getWriters(); - assertThat(writers.size()).isEqualTo(15); + assertThat(writers.size()).isEqualTo(17); assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteArrayEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteBufferEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(DataBufferEncoder.class); @@ -121,7 +127,9 @@ public class ClientCodecConfigurerTests { assertThat(writers.get(index.getAndIncrement()).getClass()).isEqualTo(ProtobufHttpMessageWriter.class); assertThat(writers.get(this.index.getAndIncrement()).getClass()).isEqualTo(MultipartHttpMessageWriter.class); assertThat(writers.get(this.index.getAndIncrement()).getClass()).isEqualTo(PartEventHttpMessageWriter.class); + assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationCborEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationJsonEncoder.class); + assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationProtobufEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2JsonEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2SmileEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jaxb2XmlEncoder.class); @@ -176,7 +184,7 @@ public class ClientCodecConfigurerTests { int size = 99; this.configurer.defaultCodecs().maxInMemorySize(size); List> readers = this.configurer.getReaders(); - assertThat(readers.size()).isEqualTo(15); + assertThat(readers.size()).isEqualTo(17); assertThat(((ByteArrayDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((ByteBufferDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((DataBufferDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); @@ -187,7 +195,9 @@ public class ClientCodecConfigurerTests { assertThat(((ProtobufDecoder) getNextDecoder(readers)).getMaxMessageSize()).isEqualTo(size); assertThat(((FormHttpMessageReader) nextReader(readers)).getMaxInMemorySize()).isEqualTo(size); + assertThat(((KotlinSerializationCborDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((KotlinSerializationJsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); + assertThat(((KotlinSerializationProtobufDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((Jackson2JsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((Jackson2SmileDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((Jaxb2XmlDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); @@ -235,7 +245,7 @@ public class ClientCodecConfigurerTests { writers = findCodec(this.configurer.getWriters(), MultipartHttpMessageWriter.class).getPartWriters(); assertThat(sseDecoder).isNotSameAs(jackson2Decoder); - assertThat(writers).hasSize(13); + assertThat(writers).hasSize(15); } @Test // gh-24194 @@ -245,7 +255,7 @@ public class ClientCodecConfigurerTests { List> writers = findCodec(clone.getWriters(), MultipartHttpMessageWriter.class).getPartWriters(); - assertThat(writers).hasSize(13); + assertThat(writers).hasSize(15); } @Test @@ -259,7 +269,7 @@ public class ClientCodecConfigurerTests { List> writers = findCodec(clone.getWriters(), MultipartHttpMessageWriter.class).getPartWriters(); - assertThat(writers).hasSize(13); + assertThat(writers).hasSize(15); } private Decoder getNextDecoder(List> readers) { diff --git a/spring-web/src/test/java/org/springframework/http/codec/support/CodecConfigurerTests.java b/spring-web/src/test/java/org/springframework/http/codec/support/CodecConfigurerTests.java index 98ab1b6576..432fe15928 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/support/CodecConfigurerTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/support/CodecConfigurerTests.java @@ -50,12 +50,16 @@ import org.springframework.http.codec.ResourceHttpMessageReader; import org.springframework.http.codec.ResourceHttpMessageWriter; import org.springframework.http.codec.ServerSentEventHttpMessageReader; import org.springframework.http.codec.ServerSentEventHttpMessageWriter; +import org.springframework.http.codec.cbor.KotlinSerializationCborDecoder; +import org.springframework.http.codec.cbor.KotlinSerializationCborEncoder; import org.springframework.http.codec.json.Jackson2JsonDecoder; import org.springframework.http.codec.json.Jackson2JsonEncoder; import org.springframework.http.codec.json.Jackson2SmileDecoder; import org.springframework.http.codec.json.Jackson2SmileEncoder; import org.springframework.http.codec.json.KotlinSerializationJsonDecoder; import org.springframework.http.codec.json.KotlinSerializationJsonEncoder; +import org.springframework.http.codec.protobuf.KotlinSerializationProtobufDecoder; +import org.springframework.http.codec.protobuf.KotlinSerializationProtobufEncoder; import org.springframework.http.codec.protobuf.ProtobufDecoder; import org.springframework.http.codec.protobuf.ProtobufEncoder; import org.springframework.http.codec.protobuf.ProtobufHttpMessageWriter; @@ -83,7 +87,7 @@ class CodecConfigurerTests { @Test void defaultReaders() { List> readers = this.configurer.getReaders(); - assertThat(readers.size()).isEqualTo(14); + assertThat(readers.size()).isEqualTo(16); assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteArrayDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteBufferDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(DataBufferDecoder.class); @@ -93,7 +97,9 @@ class CodecConfigurerTests { assertStringDecoder(getNextDecoder(readers), true); assertThat(getNextDecoder(readers).getClass()).isEqualTo(ProtobufDecoder.class); assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(FormHttpMessageReader.class); + assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationCborDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationJsonDecoder.class); + assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationProtobufDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2JsonDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2SmileDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jaxb2XmlDecoder.class); @@ -103,7 +109,7 @@ class CodecConfigurerTests { @Test void defaultWriters() { List> writers = this.configurer.getWriters(); - assertThat(writers.size()).isEqualTo(13); + assertThat(writers.size()).isEqualTo(15); assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteArrayEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteBufferEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(DataBufferEncoder.class); @@ -112,7 +118,9 @@ class CodecConfigurerTests { assertThat(writers.get(index.getAndIncrement()).getClass()).isEqualTo(ResourceHttpMessageWriter.class); assertStringEncoder(getNextEncoder(writers), true); assertThat(writers.get(index.getAndIncrement()).getClass()).isEqualTo(ProtobufHttpMessageWriter.class); + assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationCborEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationJsonEncoder.class); + assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationProtobufEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2JsonEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2SmileEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jaxb2XmlEncoder.class); @@ -141,7 +149,7 @@ class CodecConfigurerTests { List> readers = this.configurer.getReaders(); - assertThat(readers.size()).isEqualTo(18); + assertThat(readers.size()).isEqualTo(20); assertThat(getNextDecoder(readers)).isSameAs(customDecoder1); assertThat(readers.get(this.index.getAndIncrement())).isSameAs(customReader1); assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteArrayDecoder.class); @@ -155,7 +163,9 @@ class CodecConfigurerTests { assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(FormHttpMessageReader.class); assertThat(getNextDecoder(readers)).isSameAs(customDecoder2); assertThat(readers.get(this.index.getAndIncrement())).isSameAs(customReader2); + assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationCborDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationJsonDecoder.class); + assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationProtobufDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2JsonDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2SmileDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jaxb2XmlDecoder.class); @@ -184,7 +194,7 @@ class CodecConfigurerTests { List> writers = this.configurer.getWriters(); - assertThat(writers.size()).isEqualTo(17); + assertThat(writers.size()).isEqualTo(19); assertThat(getNextEncoder(writers)).isSameAs(customEncoder1); assertThat(writers.get(this.index.getAndIncrement())).isSameAs(customWriter1); assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteArrayEncoder.class); @@ -197,7 +207,9 @@ class CodecConfigurerTests { assertThat(writers.get(index.getAndIncrement()).getClass()).isEqualTo(ProtobufHttpMessageWriter.class); assertThat(getNextEncoder(writers)).isSameAs(customEncoder2); assertThat(writers.get(this.index.getAndIncrement())).isSameAs(customWriter2); + assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationCborEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationJsonEncoder.class); + assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationProtobufEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2JsonEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2SmileEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jaxb2XmlEncoder.class); diff --git a/spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java b/spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java index f1f91a511b..fe648a2216 100644 --- a/spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java +++ b/spring-web/src/test/java/org/springframework/http/codec/support/ServerCodecConfigurerTests.java @@ -54,6 +54,8 @@ import org.springframework.http.codec.ResourceHttpMessageReader; import org.springframework.http.codec.ResourceHttpMessageWriter; import org.springframework.http.codec.ServerCodecConfigurer; import org.springframework.http.codec.ServerSentEventHttpMessageWriter; +import org.springframework.http.codec.cbor.KotlinSerializationCborDecoder; +import org.springframework.http.codec.cbor.KotlinSerializationCborEncoder; import org.springframework.http.codec.json.Jackson2JsonDecoder; import org.springframework.http.codec.json.Jackson2JsonEncoder; import org.springframework.http.codec.json.Jackson2SmileDecoder; @@ -64,6 +66,8 @@ import org.springframework.http.codec.multipart.DefaultPartHttpMessageReader; import org.springframework.http.codec.multipart.MultipartHttpMessageReader; import org.springframework.http.codec.multipart.PartEventHttpMessageReader; import org.springframework.http.codec.multipart.PartHttpMessageWriter; +import org.springframework.http.codec.protobuf.KotlinSerializationProtobufDecoder; +import org.springframework.http.codec.protobuf.KotlinSerializationProtobufEncoder; import org.springframework.http.codec.protobuf.ProtobufDecoder; import org.springframework.http.codec.protobuf.ProtobufHttpMessageWriter; import org.springframework.http.codec.xml.Jaxb2XmlDecoder; @@ -88,7 +92,7 @@ public class ServerCodecConfigurerTests { @Test public void defaultReaders() { List> readers = this.configurer.getReaders(); - assertThat(readers.size()).isEqualTo(17); + assertThat(readers.size()).isEqualTo(19); assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteArrayDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(ByteBufferDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(DataBufferDecoder.class); @@ -101,7 +105,9 @@ public class ServerCodecConfigurerTests { assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(DefaultPartHttpMessageReader.class); assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(MultipartHttpMessageReader.class); assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(PartEventHttpMessageReader.class); + assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationCborDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationJsonDecoder.class); + assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationProtobufDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2JsonDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2SmileDecoder.class); assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jaxb2XmlDecoder.class); @@ -111,7 +117,7 @@ public class ServerCodecConfigurerTests { @Test public void defaultWriters() { List> writers = this.configurer.getWriters(); - assertThat(writers.size()).isEqualTo(15); + assertThat(writers.size()).isEqualTo(17); assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteArrayEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(ByteBufferEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(DataBufferEncoder.class); @@ -121,7 +127,9 @@ public class ServerCodecConfigurerTests { assertStringEncoder(getNextEncoder(writers), true); assertThat(writers.get(index.getAndIncrement()).getClass()).isEqualTo(ProtobufHttpMessageWriter.class); assertThat(writers.get(this.index.getAndIncrement()).getClass()).isEqualTo(PartHttpMessageWriter.class); + assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationCborEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationJsonEncoder.class); + assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationProtobufEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2JsonEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2SmileEncoder.class); assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jaxb2XmlEncoder.class); @@ -168,7 +176,9 @@ public class ServerCodecConfigurerTests { assertThat((reader).getMaxInMemorySize()).isEqualTo(size); assertThat(((PartEventHttpMessageReader) nextReader(readers)).getMaxInMemorySize()).isEqualTo(size); + assertThat(((KotlinSerializationCborDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((KotlinSerializationJsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); + assertThat(((KotlinSerializationProtobufDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((Jackson2JsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((Jackson2SmileDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); assertThat(((Jaxb2XmlDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size); diff --git a/spring-web/src/test/kotlin/org/springframework/http/codec/cbor/KotlinSerializationCborDecoderTests.kt b/spring-web/src/test/kotlin/org/springframework/http/codec/cbor/KotlinSerializationCborDecoderTests.kt new file mode 100644 index 0000000000..88933457bc --- /dev/null +++ b/spring-web/src/test/kotlin/org/springframework/http/codec/cbor/KotlinSerializationCborDecoderTests.kt @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2021 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.http.codec.cbor + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.cbor.Cbor +import kotlinx.serialization.serializer +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import reactor.test.StepVerifier +import reactor.test.StepVerifier.FirstStep + +import org.springframework.core.Ordered +import org.springframework.core.ResolvableType +import org.springframework.core.io.buffer.DataBuffer +import org.springframework.core.testfixture.codec.AbstractDecoderTests +import org.springframework.http.MediaType + +/** + * Tests for the JSON decoding using kotlinx.serialization. + * + * @author Sebastien Deleuze + * @author Iain Henderson + */ +@ExperimentalSerializationApi +class KotlinSerializationCborDecoderTests : AbstractDecoderTests(KotlinSerializationCborDecoder()) { + + @Suppress("UsePropertyAccessSyntax", "DEPRECATION") + @Test + override fun canDecode() { + Assertions.assertThat(decoder.canDecode(ResolvableType.forClass(Pojo::class.java), MediaType.APPLICATION_CBOR)).isTrue() + Assertions.assertThat(decoder.canDecode(ResolvableType.forClass(Pojo::class.java), null)).isTrue() + Assertions.assertThat(decoder.canDecode(ResolvableType.forClass(String::class.java), null)).isFalse() + Assertions.assertThat(decoder.canDecode(ResolvableType.forClass(Pojo::class.java), MediaType.APPLICATION_XML)).isFalse() + + Assertions.assertThat(decoder.canDecode(ResolvableType.forClassWithGenerics(List::class.java, Int::class.java), MediaType.APPLICATION_CBOR)).isTrue() + Assertions.assertThat(decoder.canDecode(ResolvableType.forClassWithGenerics(List::class.java, Ordered::class.java), MediaType.APPLICATION_CBOR)).isFalse() + Assertions.assertThat(decoder.canDecode(ResolvableType.forClassWithGenerics(List::class.java, Pojo::class.java), MediaType.APPLICATION_CBOR)).isTrue() + Assertions.assertThat(decoder.canDecode(ResolvableType.forClassWithGenerics(ArrayList::class.java, Int::class.java), MediaType.APPLICATION_CBOR)).isTrue() + Assertions.assertThat(decoder.canDecode(ResolvableType.forClassWithGenerics(ArrayList::class.java, Int::class.java), MediaType.APPLICATION_PDF)).isFalse() + Assertions.assertThat(decoder.canDecode(ResolvableType.forClass(Ordered::class.java), MediaType.APPLICATION_CBOR)).isFalse() + } + + @Test + override fun decode() { + val output = decoder.decode(Mono.empty(), ResolvableType.forClass(Pojo::class.java), null, emptyMap()) + StepVerifier + .create(output) + .expectError(UnsupportedOperationException::class.java) + } + + @Test + override fun decodeToMono() { + val pojo1 = Pojo("f1", "b1") + val input = Flux.concat( + byteBuffer(pojo1) + ) + + val elementType = ResolvableType.forClass(Pojo::class.java) + + testDecodeToMonoAll(input, elementType, { step: FirstStep -> + step + .expectNext(pojo1) + .expectComplete() + .verify() + }, null, null) + } + + private fun byteBuffer(value: Any): Mono { + return Mono.defer { + val bytes = Cbor.Default.encodeToByteArray(serializer(Pojo::class.java), value) + val buffer = bufferFactory.allocateBuffer(bytes.size) + buffer.write(bytes) + Mono.just(buffer) + } + } + +} +@Serializable +data class Pojo(val foo: String, val bar: String, val pojo: Pojo? = null) diff --git a/spring-web/src/test/kotlin/org/springframework/http/codec/cbor/KotlinSerializationCborEncoderTests.kt b/spring-web/src/test/kotlin/org/springframework/http/codec/cbor/KotlinSerializationCborEncoderTests.kt new file mode 100644 index 0000000000..0695e00247 --- /dev/null +++ b/spring-web/src/test/kotlin/org/springframework/http/codec/cbor/KotlinSerializationCborEncoderTests.kt @@ -0,0 +1,104 @@ +/* + * Copyright 2002-2021 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.http.codec.cbor + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.cbor.Cbor +import kotlinx.serialization.encodeToByteArray +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import reactor.test.StepVerifier.FirstStep + +import org.springframework.core.Ordered +import org.springframework.core.ResolvableType +import org.springframework.core.io.buffer.DataBuffer +import org.springframework.core.io.buffer.DataBufferUtils +import org.springframework.core.testfixture.codec.AbstractEncoderTests +import org.springframework.http.MediaType +import org.springframework.http.codec.ServerSentEvent + +/** + * Tests for the JSON encoding using kotlinx.serialization. + * + * @author Sebastien Deleuze + * @author Iain Henderson + */ +@Suppress("UsePropertyAccessSyntax") +@ExperimentalSerializationApi +class KotlinSerializationCborEncoderTests : AbstractEncoderTests(KotlinSerializationCborEncoder()) { + + @Test + override fun canEncode() { + val pojoType = ResolvableType.forClass(Pojo::class.java) + Assertions.assertThat(encoder.canEncode(pojoType, MediaType.APPLICATION_CBOR)).isTrue() + Assertions.assertThat(encoder.canEncode(pojoType, null)).isTrue() + + Assertions.assertThat(encoder.canEncode(ResolvableType.forClassWithGenerics(List::class.java, Int::class.java), MediaType.APPLICATION_CBOR)).isTrue() + Assertions.assertThat(encoder.canEncode(ResolvableType.forClassWithGenerics(List::class.java, Ordered::class.java), MediaType.APPLICATION_CBOR)).isFalse() + Assertions.assertThat(encoder.canEncode(ResolvableType.forClassWithGenerics(List::class.java, Pojo::class.java), MediaType.APPLICATION_CBOR)).isTrue() + Assertions.assertThat(encoder.canEncode(ResolvableType.forClassWithGenerics(ArrayList::class.java, Int::class.java), MediaType.APPLICATION_CBOR)).isTrue() + Assertions.assertThat(encoder.canEncode(ResolvableType.forClassWithGenerics(ArrayList::class.java, Int::class.java), MediaType.APPLICATION_PDF)).isFalse() + } + + @Test + override fun encode() { + val pojo1 = Pojo("foo", "bar") + val pojo2 = Pojo("foofoo", "barbar") + val pojo3 = Pojo("foofoofoo", "barbarbar") + val input = Flux.just( + pojo1, + pojo2, + pojo3 + ) + val pojoBytes = Cbor.Default.encodeToByteArray(arrayOf(pojo1, pojo2, pojo3)) + testEncode(input, Pojo::class.java) { step: FirstStep -> + step + .consumeNextWith(expectBytes(pojoBytes) + .andThen { dataBuffer: DataBuffer? -> DataBufferUtils.release(dataBuffer) }) + .verifyComplete() + } + } + + @Test + fun encodeMono() { + val pojo = Pojo("foo", "bar") + val input = Mono.just(pojo) + testEncode(input, Pojo::class.java) { step: FirstStep -> + step + .consumeNextWith(expectBytes(Cbor.Default.encodeToByteArray(pojo)) + .andThen { dataBuffer: DataBuffer? -> DataBufferUtils.release(dataBuffer) }) + .verifyComplete() + } + } + + @Test + fun canNotEncode() { + Assertions.assertThat(encoder.canEncode(ResolvableType.forClass(String::class.java), null)).isFalse() + Assertions.assertThat(encoder.canEncode(ResolvableType.forClass(Pojo::class.java), MediaType.APPLICATION_XML)).isFalse() + val sseType = ResolvableType.forClass(ServerSentEvent::class.java) + Assertions.assertThat(encoder.canEncode(sseType, MediaType.APPLICATION_CBOR)).isFalse() + Assertions.assertThat(encoder.canEncode(ResolvableType.forClass(Ordered::class.java), MediaType.APPLICATION_CBOR)).isFalse() + } + + + @Serializable + data class Pojo(val foo: String, val bar: String, val pojo: Pojo? = null) + +} diff --git a/spring-web/src/test/kotlin/org/springframework/http/codec/protobuf/KotlinSerializationProtobufDecoderTests.kt b/spring-web/src/test/kotlin/org/springframework/http/codec/protobuf/KotlinSerializationProtobufDecoderTests.kt new file mode 100644 index 0000000000..5fcbeaa357 --- /dev/null +++ b/spring-web/src/test/kotlin/org/springframework/http/codec/protobuf/KotlinSerializationProtobufDecoderTests.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2002-2022 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.http.codec.protobuf + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.protobuf.ProtoBuf +import kotlinx.serialization.serializer +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test +import org.springframework.core.Ordered +import org.springframework.core.ResolvableType +import org.springframework.core.io.buffer.DataBuffer +import org.springframework.core.testfixture.codec.AbstractDecoderTests +import org.springframework.http.MediaType +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import reactor.test.StepVerifier +import reactor.test.StepVerifier.FirstStep + +/** + * Tests for the JSON decoding using kotlinx.serialization. + * + * @author Sebastien Deleuze + * @author Iain Henderson + */ +@ExperimentalSerializationApi +class KotlinSerializationCborDecoderTests : AbstractDecoderTests(KotlinSerializationProtobufDecoder()) { + @Suppress("UsePropertyAccessSyntax") + @Test + override fun canDecode() { + for (mimeType in listOf(MediaType.APPLICATION_PROTOBUF, MediaType.APPLICATION_OCTET_STREAM, MediaType("application", "vnd.google.protobuf"))) { + Assertions.assertThat(decoder.canDecode(ResolvableType.forClass(Pojo::class.java),mimeType)).isTrue() + + Assertions.assertThat(decoder.canDecode(ResolvableType.forClassWithGenerics(List::class.java, Int::class.java), mimeType)).isTrue() + Assertions.assertThat(decoder.canDecode(ResolvableType.forClassWithGenerics(List::class.java, Ordered::class.java), mimeType)).isFalse() + Assertions.assertThat(decoder.canDecode(ResolvableType.forClassWithGenerics(List::class.java, Pojo::class.java), mimeType)).isTrue() + Assertions.assertThat(decoder.canDecode(ResolvableType.forClassWithGenerics(ArrayList::class.java, Int::class.java), mimeType)).isTrue() + Assertions.assertThat(decoder.canDecode(ResolvableType.forClass(Ordered::class.java), mimeType)).isFalse() + } + Assertions.assertThat(decoder.canDecode(ResolvableType.forClass(Pojo::class.java), null)).isTrue() + Assertions.assertThat(decoder.canDecode(ResolvableType.forClass(String::class.java), null)).isFalse() + Assertions.assertThat(decoder.canDecode(ResolvableType.forClass(Pojo::class.java), MediaType.APPLICATION_XML)).isFalse() + Assertions.assertThat(decoder.canDecode(ResolvableType.forClassWithGenerics(ArrayList::class.java, Int::class.java), MediaType.APPLICATION_PDF)).isFalse() + } + + @Test + override fun decode() { + val output = decoder.decode(Mono.empty(), ResolvableType.forClass(Pojo::class.java), null, emptyMap()) + StepVerifier + .create(output) + .expectError(UnsupportedOperationException::class.java) + .verify(); + } + + @Test + override fun decodeToMono() { + val pojo1 = Pojo("f1", "b1") + val pojo2 = Pojo("f2", "b2") + val input = Flux.concat( + byteBuffer(pojo1), + byteBuffer(pojo2) + ) + + val elementType = ResolvableType.forClass(Pojo::class.java) + + testDecodeToMonoAll(input, elementType, { step: FirstStep -> + step + .expectNext(pojo2) + .expectComplete() + .verify() + }, null, null) + } + + private fun byteBuffer(value: Any): Mono { + return Mono.defer { + val bytes = ProtoBuf.Default.encodeToByteArray(serializer(Pojo::class.java), value) + val buffer = bufferFactory.allocateBuffer(bytes.size) + buffer.write(bytes) + Mono.just(buffer) + } + } + +} +@Serializable +data class Pojo(val foo: String, val bar: String, val pojo: Pojo? = null) diff --git a/spring-web/src/test/kotlin/org/springframework/http/codec/protobuf/KotlinSerializationProtobufEncoderTests.kt b/spring-web/src/test/kotlin/org/springframework/http/codec/protobuf/KotlinSerializationProtobufEncoderTests.kt new file mode 100644 index 0000000000..fdd86e46cd --- /dev/null +++ b/spring-web/src/test/kotlin/org/springframework/http/codec/protobuf/KotlinSerializationProtobufEncoderTests.kt @@ -0,0 +1,115 @@ +/* + * Copyright 2002-2022 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.http.codec.protobuf + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToByteArray +import kotlinx.serialization.protobuf.ProtoBuf +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test +import org.springframework.core.Ordered +import org.springframework.core.ResolvableType +import org.springframework.core.io.buffer.DataBuffer +import org.springframework.core.io.buffer.DataBufferUtils +import org.springframework.core.testfixture.codec.AbstractEncoderTests +import org.springframework.http.MediaType +import org.springframework.http.codec.ServerSentEvent +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import reactor.test.StepVerifier.FirstStep + +/** + * Tests for the JSON encoding using kotlinx.serialization. + * + * @author Sebastien Deleuze + * @author Iain Henderson + */ +@Suppress("UsePropertyAccessSyntax") +@ExperimentalSerializationApi +class KotlinSerializationProtobufEncoderTests : AbstractEncoderTests(KotlinSerializationProtobufEncoder()) { + + private val mediaTypes = listOf( + MediaType.APPLICATION_PROTOBUF, + MediaType.APPLICATION_OCTET_STREAM, + MediaType("application", "vnd.google.protobuf") + ) + + @Test + override fun canEncode() { + val pojoType = ResolvableType.forClass(Pojo::class.java) + Assertions.assertThat(encoder.canEncode(pojoType, null)).isTrue() + + for (mimeType in ProtobufCodecSupport.MIME_TYPES) { + val mediaType = MediaType(mimeType) + Assertions.assertThat(encoder.canEncode(pojoType, mediaType)).isTrue() + + Assertions.assertThat(encoder.canEncode(ResolvableType.forClassWithGenerics(List::class.java, Int::class.java), mimeType)).isTrue() + Assertions.assertThat(encoder.canEncode(ResolvableType.forClassWithGenerics(List::class.java, Ordered::class.java), mimeType)).isFalse() + Assertions.assertThat(encoder.canEncode(ResolvableType.forClassWithGenerics(List::class.java, Pojo::class.java), mimeType)).isTrue() + Assertions.assertThat(encoder.canEncode(ResolvableType.forClassWithGenerics(ArrayList::class.java, Int::class.java), mimeType)).isTrue() + } + Assertions.assertThat(encoder.canEncode(ResolvableType.forClassWithGenerics(ArrayList::class.java, Int::class.java), MediaType.APPLICATION_PDF)).isFalse() + } + + @Test + override fun encode() { + val pojo1 = Pojo("foo", "bar") + val pojo2 = Pojo("foofoo", "barbar") + val pojo3 = Pojo("foofoofoo", "barbarbar") + val input = Flux.just( + pojo1, + pojo2, + pojo3 + ) + val pojoBytes = ProtoBuf.Default.encodeToByteArray(arrayOf(pojo1, pojo2, pojo3)) + testEncode(input, Pojo::class.java) { step: FirstStep -> + step + .consumeNextWith(expectBytes(pojoBytes) + .andThen { dataBuffer: DataBuffer? -> DataBufferUtils.release(dataBuffer) }) + .verifyComplete() + } + } + + @Test + fun encodeMono() { + val pojo = Pojo("foo", "bar") + val input = Mono.just(pojo) + testEncode(input, Pojo::class.java) { step: FirstStep -> + step + .consumeNextWith(expectBytes(ProtoBuf.Default.encodeToByteArray(pojo)) + .andThen { dataBuffer: DataBuffer? -> DataBufferUtils.release(dataBuffer) }) + .verifyComplete() + } + } + + @Test + fun canNotEncode() { + Assertions.assertThat(encoder.canEncode(ResolvableType.forClass(String::class.java), null)).isFalse() + Assertions.assertThat(encoder.canEncode(ResolvableType.forClass(Pojo::class.java), MediaType.APPLICATION_XML)).isFalse() + val sseType = ResolvableType.forClass(ServerSentEvent::class.java) + for (mediaType in mediaTypes) { + Assertions.assertThat(encoder.canEncode(sseType, mediaType)).isFalse() + Assertions.assertThat(encoder.canEncode(ResolvableType.forClass(Ordered::class.java), mediaType)).isFalse() + } + } + + + @Serializable + data class Pojo(val foo: String, val bar: String, val pojo: Pojo? = null) + +} diff --git a/spring-web/src/test/kotlin/org/springframework/http/converter/cbor/KotlinSerializationCborHttpMessageConverterTests.kt b/spring-web/src/test/kotlin/org/springframework/http/converter/cbor/KotlinSerializationCborHttpMessageConverterTests.kt new file mode 100644 index 0000000000..15f457a825 --- /dev/null +++ b/spring-web/src/test/kotlin/org/springframework/http/converter/cbor/KotlinSerializationCborHttpMessageConverterTests.kt @@ -0,0 +1,231 @@ +/* + * Copyright 2002-2021 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.http.converter.cbor + +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type +import java.nio.charset.StandardCharsets + +import kotlin.reflect.javaType +import kotlin.reflect.typeOf +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.cbor.Cbor +import kotlinx.serialization.encodeToByteArray +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatExceptionOfType +import org.junit.jupiter.api.Test + +import org.springframework.core.Ordered +import org.springframework.http.MediaType +import org.springframework.http.MockHttpInputMessage +import org.springframework.http.MockHttpOutputMessage +import org.springframework.http.converter.HttpMessageNotReadableException + +/** + * Tests for the CBOR conversion using kotlinx.serialization. + * + * @author Andreas Ahlenstorf + * @author Sebastien Deleuze + * @author Iain Henderson + */ +@Suppress("UsePropertyAccessSyntax") +@ExperimentalSerializationApi +class KotlinSerializationCborHttpMessageConverterTests { + + private val converter = KotlinSerializationCborHttpMessageConverter() + private val serializableBean = SerializableBean( + bytes = byteArrayOf(0x1, 0x2), + array = arrayOf("Foo", "Bar"), + number = 42, + string = "Foo", + bool = true, + fraction = 42f + ) + private val serializableBeanArray = arrayOf(serializableBean) + private val serializableBeanArrayBody = Cbor.Default.encodeToByteArray(serializableBeanArray) + + @Test + fun canReadCbor() { + assertThat(converter.canRead(SerializableBean::class.java, MediaType.APPLICATION_CBOR)).isTrue() + assertThat(converter.canRead(SerializableBean::class.java, MediaType.APPLICATION_JSON)).isFalse() + assertThat(converter.canRead(String::class.java, MediaType.APPLICATION_CBOR)).isTrue() + assertThat(converter.canRead(NotSerializableBean::class.java, MediaType.APPLICATION_CBOR)).isFalse() + + assertThat(converter.canRead(Map::class.java, MediaType.APPLICATION_CBOR)).isFalse() + assertThat(converter.canRead(typeTokenOf>(), Map::class.java, MediaType.APPLICATION_CBOR)).isTrue() + assertThat(converter.canRead(List::class.java, MediaType.APPLICATION_CBOR)).isFalse() + assertThat(converter.canRead(typeTokenOf>(), List::class.java, MediaType.APPLICATION_CBOR)).isTrue() + assertThat(converter.canRead(Set::class.java, MediaType.APPLICATION_CBOR)).isFalse() + assertThat(converter.canRead(typeTokenOf>(), Set::class.java, MediaType.APPLICATION_CBOR)).isTrue() + + assertThat(converter.canRead(typeTokenOf>(), List::class.java, MediaType.APPLICATION_CBOR)).isTrue() + assertThat(converter.canRead(typeTokenOf>(), List::class.java, MediaType.APPLICATION_CBOR)).isTrue() + assertThat(converter.canRead(typeTokenOf>(), List::class.java, MediaType.APPLICATION_JSON)).isFalse() + + assertThat(converter.canRead(typeTokenOf(), Ordered::class.java, MediaType.APPLICATION_CBOR)).isFalse() + assertThat(converter.canRead(typeTokenOf>(), List::class.java, MediaType.APPLICATION_CBOR)).isFalse() + } + + @Test + fun canWriteCbor() { + assertThat(converter.canWrite(SerializableBean::class.java, MediaType.APPLICATION_CBOR)).isTrue() + assertThat(converter.canWrite(SerializableBean::class.java, MediaType.APPLICATION_JSON)).isFalse() + assertThat(converter.canWrite(String::class.java, MediaType.APPLICATION_CBOR)).isTrue() + assertThat(converter.canWrite(NotSerializableBean::class.java, MediaType.APPLICATION_CBOR)).isFalse() + + assertThat(converter.canWrite(Map::class.java, MediaType.APPLICATION_CBOR)).isFalse() + assertThat(converter.canWrite(typeTokenOf>(), Map::class.java, MediaType.APPLICATION_CBOR)).isTrue() + assertThat(converter.canWrite(List::class.java, MediaType.APPLICATION_CBOR)).isFalse() + assertThat(converter.canWrite(typeTokenOf>(), List::class.java, MediaType.APPLICATION_CBOR)).isTrue() + assertThat(converter.canWrite(Set::class.java, MediaType.APPLICATION_CBOR)).isFalse() + assertThat(converter.canWrite(typeTokenOf>(), Set::class.java, MediaType.APPLICATION_CBOR)).isTrue() + + assertThat(converter.canWrite(typeTokenOf>(), List::class.java, MediaType.APPLICATION_CBOR)).isTrue() + assertThat(converter.canWrite(typeTokenOf>(), List::class.java, MediaType.APPLICATION_CBOR)).isTrue() + assertThat(converter.canWrite(typeTokenOf>(), List::class.java, MediaType.APPLICATION_JSON)).isFalse() + + assertThat(converter.canWrite(typeTokenOf(), Ordered::class.java, MediaType.APPLICATION_CBOR)).isFalse() + } + + @Test + fun readObject() { + val serializableBeanBody = Cbor.Default.encodeToByteArray(serializableBean) + val inputMessage = MockHttpInputMessage(serializableBeanBody) + inputMessage.headers.contentType = MediaType.APPLICATION_CBOR + val result = converter.read(SerializableBean::class.java, inputMessage) as SerializableBean + + assertThat(result.bytes).containsExactly(*serializableBean.bytes) + assertThat(result.array).containsExactly(*serializableBean.array) + assertThat(result.number).isEqualTo(serializableBean.number) + assertThat(result.string).isEqualTo(serializableBean.string) + assertThat(result.bool).isTrue() + assertThat(result.fraction).isEqualTo(serializableBean.fraction) + } + + @Test + @Suppress("UNCHECKED_CAST") + fun readArrayOfObjects() { + val inputMessage = MockHttpInputMessage(serializableBeanArrayBody) + inputMessage.headers.contentType = MediaType.APPLICATION_CBOR + val result = converter.read(Array::class.java, inputMessage) as Array + + assertThat(result).hasSize(1) + assertThat(result[0].bytes).containsExactly(*serializableBean.bytes) + assertThat(result[0].array).containsExactly(*serializableBean.array) + assertThat(result[0].number).isEqualTo(serializableBean.number) + assertThat(result[0].string).isEqualTo(serializableBean.string) + assertThat(result[0].bool).isTrue() + assertThat(result[0].fraction).isEqualTo(serializableBean.fraction) + } + + @Test + @Suppress("UNCHECKED_CAST") + @ExperimentalStdlibApi + fun readGenericCollection() { + val inputMessage = MockHttpInputMessage(serializableBeanArrayBody) + inputMessage.headers.contentType = MediaType.APPLICATION_CBOR + val result = converter.read(typeOf>().javaType, null, inputMessage) + as List + + assertThat(result).hasSize(1) + assertThat(result[0].bytes).containsExactly(*serializableBean.bytes) + assertThat(result[0].array).containsExactly(*serializableBean.array) + assertThat(result[0].number).isEqualTo(serializableBean.number) + assertThat(result[0].string).isEqualTo(serializableBean.string) + assertThat(result[0].bool).isTrue() + assertThat(result[0].fraction).isEqualTo(serializableBean.fraction) + } + + @Test + fun readFailsOnInvalidCbor() { + val body = """ + this is an invalid JSON document and definitely NOT CBOR + """.trimIndent() + + val inputMessage = MockHttpInputMessage(body.toByteArray(StandardCharsets.UTF_8)) + inputMessage.headers.contentType = MediaType.APPLICATION_CBOR + assertThatExceptionOfType(HttpMessageNotReadableException::class.java).isThrownBy { + converter.read(SerializableBean::class.java, inputMessage) + } + } + + @Test + fun writeObject() { + val outputMessage = MockHttpOutputMessage() + + this.converter.write(serializableBean, null, outputMessage) + + assertThat(outputMessage.headers).containsEntry("Content-Type", listOf("application/cbor")) + assertThat(outputMessage.bodyAsBytes.isNotEmpty()).isTrue() + } + + @Test + fun writeObjectWithNullableProperty() { + val outputMessage = MockHttpOutputMessage() + val serializableBean = SerializableBean(byteArrayOf(0x1, 0x2), arrayOf("Foo", "Bar"), 42, null, true, 42.0f) + + this.converter.write(serializableBean, null, outputMessage) + + assertThat(outputMessage.headers).containsEntry("Content-Type", listOf("application/cbor")) + assertThat(outputMessage.bodyAsBytes.isNotEmpty()).isTrue() + } + + @Test + fun writeArrayOfObjects() { + val outputMessage = MockHttpOutputMessage() + + this.converter.write(serializableBeanArray, null, outputMessage) + + assertThat(outputMessage.headers).containsEntry("Content-Type", listOf("application/cbor")) + assertThat(outputMessage.bodyAsBytes.isNotEmpty()).isTrue() + } + + @Test + @ExperimentalStdlibApi + fun writeGenericCollection() { + val outputMessage = MockHttpOutputMessage() + + this.converter.write(listOf(serializableBean), typeOf>().javaType, null, outputMessage) + + assertThat(outputMessage.headers).containsEntry("Content-Type", listOf("application/cbor")) + assertThat(outputMessage.bodyAsBytes.isNotEmpty()).isTrue() + } + + @Serializable + @Suppress("ArrayInDataClass") + data class SerializableBean( + val bytes: ByteArray, + val array: Array, + val number: Int, + val string: String?, + val bool: Boolean, + val fraction: Float, + val serializableBean: SerializableBean? = null + ) + + data class NotSerializableBean(val string: String) + + open class TypeBase + + inline fun typeTokenOf(): Type { + val base = object : TypeBase() {} + val superType = base::class.java.genericSuperclass!! + return (superType as ParameterizedType).actualTypeArguments.first()!! + } + +} diff --git a/spring-web/src/test/kotlin/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverterTests.kt b/spring-web/src/test/kotlin/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverterTests.kt index 7bb1436d39..de7789d744 100644 --- a/spring-web/src/test/kotlin/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverterTests.kt +++ b/spring-web/src/test/kotlin/org/springframework/http/converter/json/KotlinSerializationJsonHttpMessageConverterTests.kt @@ -16,21 +16,23 @@ package org.springframework.http.converter.json +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type +import java.nio.charset.StandardCharsets + import kotlinx.serialization.Serializable import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatExceptionOfType import org.junit.jupiter.api.Test +import kotlin.reflect.javaType +import kotlin.reflect.typeOf + import org.springframework.core.Ordered import org.springframework.core.ResolvableType import org.springframework.http.MediaType import org.springframework.http.MockHttpInputMessage import org.springframework.http.MockHttpOutputMessage import org.springframework.http.converter.HttpMessageNotReadableException -import java.lang.reflect.ParameterizedType -import java.lang.reflect.Type -import java.nio.charset.StandardCharsets -import kotlin.reflect.javaType -import kotlin.reflect.typeOf /** * Tests for the JSON conversion using kotlinx.serialization. diff --git a/spring-web/src/test/kotlin/org/springframework/http/converter/protobuf/KotlinSerializationProtobufHttpMessageConverterTests.kt b/spring-web/src/test/kotlin/org/springframework/http/converter/protobuf/KotlinSerializationProtobufHttpMessageConverterTests.kt new file mode 100644 index 0000000000..5e12b0f29c --- /dev/null +++ b/spring-web/src/test/kotlin/org/springframework/http/converter/protobuf/KotlinSerializationProtobufHttpMessageConverterTests.kt @@ -0,0 +1,247 @@ +/* + * Copyright 2002-2022 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.http.converter.protobuf + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToByteArray +import kotlinx.serialization.protobuf.ProtoBuf +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatExceptionOfType +import org.junit.jupiter.api.Test +import org.springframework.core.Ordered +import org.springframework.http.MediaType +import org.springframework.http.MockHttpInputMessage +import org.springframework.http.MockHttpOutputMessage +import org.springframework.http.converter.HttpMessageNotReadableException +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type +import java.nio.charset.StandardCharsets +import kotlin.reflect.javaType +import kotlin.reflect.typeOf + +/** + * Tests for the Protocol Buffer conversion using kotlinx.serialization. + * + * @author Andreas Ahlenstorf + * @author Sebastien Deleuze + * @author Iain Henderson + */ +@Suppress("UsePropertyAccessSyntax") +@ExperimentalSerializationApi +class KotlinSerializationProtobufHttpMessageConverterTests { + + private val converter = KotlinSerializationProtobufHttpMessageConverter() + private val mediaTypes = listOf( + MediaType.APPLICATION_PROTOBUF, + MediaType.APPLICATION_OCTET_STREAM, + MediaType("application", "vnd.google.protobuf") + ) + private val serializableBean = SerializableBean( + bytes = byteArrayOf(0x1, 0x2), + array = arrayOf("Foo", "Bar"), + number = 42, + string = "Foo", + bool = true, + fraction = 42f + ) + private val serializableBeanArray = arrayOf(serializableBean) + private val serializableBeanArrayBody = ProtoBuf.Default.encodeToByteArray(serializableBeanArray) + @Test + fun canReadProtobuf() { + for (mimeType in mediaTypes) { + assertThat(converter.canRead(SerializableBean::class.java, mimeType)).isTrue() + assertThat(converter.canRead(String::class.java, mimeType)).isTrue() + assertThat(converter.canRead(NotSerializableBean::class.java, mimeType)).isFalse() + + assertThat(converter.canRead(Map::class.java, mimeType)).isFalse() + assertThat(converter.canRead(typeTokenOf>(), Map::class.java, mimeType)).isTrue() + assertThat(converter.canRead(List::class.java, mimeType)).isFalse() + assertThat(converter.canRead(typeTokenOf>(), List::class.java, mimeType)).isTrue() + assertThat(converter.canRead(Set::class.java, mimeType)).isFalse() + assertThat(converter.canRead(typeTokenOf>(), Set::class.java, mimeType)).isTrue() + + assertThat(converter.canRead(typeTokenOf>(), List::class.java, mimeType)).isTrue() + assertThat(converter.canRead(typeTokenOf>(), List::class.java, mimeType)).isTrue() + + assertThat(converter.canRead(typeTokenOf(), Ordered::class.java, mimeType)).isFalse() + assertThat(converter.canRead(typeTokenOf>(), List::class.java, mimeType)).isFalse() + } + assertThat(converter.canRead(SerializableBean::class.java, MediaType.APPLICATION_JSON)).isFalse() + assertThat(converter.canRead(typeTokenOf>(), List::class.java, MediaType.APPLICATION_JSON)).isFalse() + } + + @Test + fun canWriteProtobuf() { + for (mimeType in mediaTypes) { + assertThat(converter.canWrite(SerializableBean::class.java, mimeType)).isTrue() + assertThat(converter.canWrite(String::class.java, mimeType)).isTrue() + assertThat(converter.canWrite(NotSerializableBean::class.java, mimeType)).isFalse() + + assertThat(converter.canWrite(Map::class.java, mimeType)).isFalse() + assertThat(converter.canWrite(typeTokenOf>(), Map::class.java, mimeType)).isTrue() + assertThat(converter.canWrite(List::class.java, mimeType)).isFalse() + assertThat(converter.canWrite(typeTokenOf>(), List::class.java, mimeType)).isTrue() + assertThat(converter.canWrite(Set::class.java, mimeType)).isFalse() + assertThat(converter.canWrite(typeTokenOf>(), Set::class.java, mimeType)).isTrue() + + assertThat(converter.canWrite(typeTokenOf>(), List::class.java, mimeType)).isTrue() + assertThat(converter.canWrite(typeTokenOf>(), List::class.java, mimeType)).isTrue() + + assertThat(converter.canWrite(typeTokenOf(), Ordered::class.java, mimeType)).isFalse() + } + + assertThat(converter.canWrite(SerializableBean::class.java, MediaType.APPLICATION_JSON)).isFalse() + assertThat(converter.canWrite(typeTokenOf>(), List::class.java, MediaType.APPLICATION_JSON)).isFalse() + } + + @Test + fun readObject() { + val serializableBeanBody = ProtoBuf.Default.encodeToByteArray(serializableBean) + + for (mimeType in mediaTypes) { + val inputMessage = MockHttpInputMessage(serializableBeanBody) + inputMessage.headers.contentType = mimeType + val result = converter.read(SerializableBean::class.java, inputMessage) as SerializableBean + + assertThat(result.bytes).containsExactly(*serializableBean.bytes) + assertThat(result.array).containsExactly(*serializableBean.array) + assertThat(result.number).isEqualTo(serializableBean.number) + assertThat(result.string).isEqualTo(serializableBean.string) + assertThat(result.bool).isTrue() + assertThat(result.fraction).isEqualTo(serializableBean.fraction) + } + } + + @Test + @Suppress("UNCHECKED_CAST") + fun readArrayOfObjects() { + for (mimeType in mediaTypes) { + val inputMessage = MockHttpInputMessage(serializableBeanArrayBody) + inputMessage.headers.contentType = mimeType + val result = converter.read(Array::class.java, inputMessage) as Array + + assertThat(result).hasSize(1) + assertThat(result[0].bytes).containsExactly(*serializableBean.bytes) + assertThat(result[0].array).containsExactly(*serializableBean.array) + assertThat(result[0].number).isEqualTo(serializableBean.number) + assertThat(result[0].string).isEqualTo(serializableBean.string) + assertThat(result[0].bool).isTrue() + assertThat(result[0].fraction).isEqualTo(serializableBean.fraction) + } + } + + @Test + @Suppress("UNCHECKED_CAST") + @ExperimentalStdlibApi + fun readGenericCollection() { + for (mimeType in mediaTypes) { + val inputMessage = MockHttpInputMessage(serializableBeanArrayBody) + inputMessage.headers.contentType = mimeType + val result = converter.read(typeOf>().javaType, null, inputMessage) + as List + + assertThat(result).hasSize(1) + assertThat(result[0].bytes).containsExactly(*serializableBean.bytes) + assertThat(result[0].array).containsExactly(*serializableBean.array) + assertThat(result[0].number).isEqualTo(serializableBean.number) + assertThat(result[0].string).isEqualTo(serializableBean.string) + assertThat(result[0].bool).isTrue() + assertThat(result[0].fraction).isEqualTo(serializableBean.fraction) + } + } + + @Test + fun readFailsOnInvalidProtobuf() { + val body = """ + this is an invalid JSON document and definitely NOT a Protocol Buffer + """.trimIndent() + + for (mimeType in mediaTypes) { + val inputMessage = MockHttpInputMessage(body.toByteArray(StandardCharsets.UTF_8)) + inputMessage.headers.contentType = mimeType + assertThatExceptionOfType(HttpMessageNotReadableException::class.java).isThrownBy { + converter.read(SerializableBean::class.java, inputMessage) + } + } + } + + @Test + fun writeObject() { + val outputMessage = MockHttpOutputMessage() + + this.converter.write(serializableBean, null, outputMessage) + + assertThat(outputMessage.headers).containsEntry("Content-Type", listOf("application/x-protobuf")) + assertThat(outputMessage.bodyAsBytes.isNotEmpty()).isTrue() + } + + @Test + fun writeObjectWithNullableProperty() { + val outputMessage = MockHttpOutputMessage() + val serializableBean = SerializableBean(byteArrayOf(0x1, 0x2), arrayOf("Foo", "Bar"), 42, null, true, 42.0f) + + this.converter.write(serializableBean, null, outputMessage) + + assertThat(outputMessage.headers).containsEntry("Content-Type", listOf("application/x-protobuf")) + assertThat(outputMessage.bodyAsBytes.isNotEmpty()).isTrue() + } + + @Test + fun writeArrayOfObjects() { + val outputMessage = MockHttpOutputMessage() + + this.converter.write(serializableBeanArray, null, outputMessage) + + assertThat(outputMessage.headers).containsEntry("Content-Type", listOf("application/x-protobuf")) + assertThat(outputMessage.bodyAsBytes.isNotEmpty()).isTrue() + } + + @Test + @ExperimentalStdlibApi + fun writeGenericCollection() { + val outputMessage = MockHttpOutputMessage() + + this.converter.write(listOf(serializableBean), typeOf>().javaType, null, outputMessage) + + assertThat(outputMessage.headers).containsEntry("Content-Type", listOf("application/x-protobuf")) + assertThat(outputMessage.bodyAsBytes.isNotEmpty()).isTrue() + } + + @Serializable + @Suppress("ArrayInDataClass") + data class SerializableBean( + val bytes: ByteArray, + val array: Array, + val number: Int, + val string: String?, + val bool: Boolean, + val fraction: Float, + val serializableBean: SerializableBean? = null + ) + + data class NotSerializableBean(val string: String) + + open class TypeBase + + inline fun typeTokenOf(): Type { + val base = object : TypeBase() {} + val superType = base::class.java.genericSuperclass!! + return (superType as ParameterizedType).actualTypeArguments.first()!! + } + +} diff --git a/spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxConfigurationSupportTests.java b/spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxConfigurationSupportTests.java index 385cf154d2..777cbec753 100644 --- a/spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxConfigurationSupportTests.java +++ b/spring-webflux/src/test/java/org/springframework/web/reactive/config/WebFluxConfigurationSupportTests.java @@ -83,6 +83,7 @@ import static org.springframework.core.ResolvableType.forClassWithGenerics; import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED; import static org.springframework.http.MediaType.APPLICATION_JSON; import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM; +import static org.springframework.http.MediaType.APPLICATION_PROTOBUF; import static org.springframework.http.MediaType.APPLICATION_XML; import static org.springframework.http.MediaType.IMAGE_PNG; import static org.springframework.http.MediaType.TEXT_PLAIN; @@ -152,7 +153,7 @@ public class WebFluxConfigurationSupportTests { assertHasMessageReader(readers, forClass(ByteBuffer.class), APPLICATION_OCTET_STREAM); assertHasMessageReader(readers, forClass(String.class), TEXT_PLAIN); assertHasMessageReader(readers, forClass(Resource.class), IMAGE_PNG); - assertHasMessageReader(readers, forClass(Message.class), new MediaType("application", "x-protobuf")); + assertHasMessageReader(readers, forClass(Message.class), APPLICATION_PROTOBUF); assertHasMessageReader(readers, multiValueMapType, APPLICATION_FORM_URLENCODED); assertHasMessageReader(readers, forClass(TestBean.class), APPLICATION_XML); assertHasMessageReader(readers, forClass(TestBean.class), APPLICATION_JSON); @@ -205,7 +206,7 @@ public class WebFluxConfigurationSupportTests { assertHasMessageWriter(writers, forClass(ByteBuffer.class), APPLICATION_OCTET_STREAM); assertHasMessageWriter(writers, forClass(String.class), TEXT_PLAIN); assertHasMessageWriter(writers, forClass(Resource.class), IMAGE_PNG); - assertHasMessageWriter(writers, forClass(Message.class), new MediaType("application", "x-protobuf")); + assertHasMessageWriter(writers, forClass(Message.class), APPLICATION_PROTOBUF); assertHasMessageWriter(writers, forClass(TestBean.class), APPLICATION_XML); assertHasMessageWriter(writers, forClass(TestBean.class), APPLICATION_JSON); assertHasMessageWriter(writers, forClass(TestBean.class), new MediaType("application", "x-jackson-smile")); @@ -233,7 +234,7 @@ public class WebFluxConfigurationSupportTests { assertHasMessageWriter(writers, forClass(ByteBuffer.class), APPLICATION_OCTET_STREAM); assertHasMessageWriter(writers, forClass(String.class), TEXT_PLAIN); assertHasMessageWriter(writers, forClass(Resource.class), IMAGE_PNG); - assertHasMessageWriter(writers, forClass(Message.class), new MediaType("application", "x-protobuf")); + assertHasMessageWriter(writers, forClass(Message.class), APPLICATION_PROTOBUF); assertHasMessageWriter(writers, forClass(TestBean.class), APPLICATION_XML); assertHasMessageWriter(writers, forClass(TestBean.class), APPLICATION_JSON); assertHasMessageWriter(writers, forClass(TestBean.class), new MediaType("application", "x-jackson-smile")); diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java index 3394b8eed1..80647f21c8 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java @@ -45,6 +45,7 @@ import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.ResourceHttpMessageConverter; import org.springframework.http.converter.ResourceRegionHttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.converter.cbor.KotlinSerializationCborHttpMessageConverter; import org.springframework.http.converter.cbor.MappingJackson2CborHttpMessageConverter; import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter; import org.springframework.http.converter.feed.RssChannelHttpMessageConverter; @@ -53,6 +54,7 @@ import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import org.springframework.http.converter.json.JsonbHttpMessageConverter; import org.springframework.http.converter.json.KotlinSerializationJsonHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.http.converter.protobuf.KotlinSerializationProtobufHttpMessageConverter; import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter; import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; @@ -211,8 +213,12 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv private static final boolean jsonbPresent; + private static final boolean kotlinSerializationCborPresent; + private static final boolean kotlinSerializationJsonPresent; + private static final boolean kotlinSerializationProtobufPresent; + static { ClassLoader classLoader = WebMvcConfigurationSupport.class.getClassLoader(); romePresent = ClassUtils.isPresent("com.rometools.rome.feed.WireFeed", classLoader); @@ -224,7 +230,9 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader); gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader); jsonbPresent = ClassUtils.isPresent("jakarta.json.bind.Jsonb", classLoader); + kotlinSerializationCborPresent = ClassUtils.isPresent("kotlinx.serialization.cbor.Cbor", classLoader); kotlinSerializationJsonPresent = ClassUtils.isPresent("kotlinx.serialization.json.Json", classLoader); + kotlinSerializationProtobufPresent = ClassUtils.isPresent("kotlinx.serialization.protobuf.ProtoBuf", classLoader); } @@ -455,13 +463,13 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv if (!shouldIgnoreXml && (jaxb2Present || jackson2XmlPresent)) { map.put("xml", MediaType.APPLICATION_XML); } - if (jackson2Present || gsonPresent || jsonbPresent) { + if (jackson2Present || gsonPresent || jsonbPresent || kotlinSerializationJsonPresent) { map.put("json", MediaType.APPLICATION_JSON); } if (jackson2SmilePresent) { map.put("smile", MediaType.valueOf("application/x-jackson-smile")); } - if (jackson2CborPresent) { + if (jackson2CborPresent || kotlinSerializationCborPresent) { map.put("cbor", MediaType.APPLICATION_CBOR); } return map; @@ -908,9 +916,16 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv } } + if (kotlinSerializationCborPresent) { + messageConverters.add(new KotlinSerializationCborHttpMessageConverter()); + } if (kotlinSerializationJsonPresent) { messageConverters.add(new KotlinSerializationJsonHttpMessageConverter()); } + if (kotlinSerializationProtobufPresent) { + messageConverters.add(new KotlinSerializationProtobufHttpMessageConverter()); + } + if (jackson2Present) { Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json(); if (this.applicationContext != null) { diff --git a/src/docs/asciidoc/languages/kotlin.adoc b/src/docs/asciidoc/languages/kotlin.adoc index 56dd821057..d7ae799fdd 100644 --- a/src/docs/asciidoc/languages/kotlin.adoc +++ b/src/docs/asciidoc/languages/kotlin.adoc @@ -389,7 +389,7 @@ project for more details. === Kotlin multiplatform serialization As of Spring Framework 5.3, https://github.com/Kotlin/kotlinx.serialization[Kotlin multiplatform serialization] is -supported in Spring MVC, Spring WebFlux and Spring Messaging (RSocket). The builtin support currently only targets JSON format. +supported in Spring MVC, Spring WebFlux and Spring Messaging (RSocket). The builtin support currently targets CBOR, JSON, and ProtoBuf formats. To enable it, follow https://github.com/Kotlin/kotlinx.serialization#setup[those instructions] to add the related dependency and plugin. With Spring MVC and WebFlux, both Kotlin serialization and Jackson will be configured by default if they are in the classpath since