Support CBOR and Protobuf with Kotlin Serialization
This commit introduces support for CBOR and Protobuf using Kotlin serialization. Support comes in the form of Encoder/Decoder as well as HttpMessageConverters. Seperate abstract base classes supply support for binary and string (de)serialization. The exising JSON codecs and message converters have been migrated to use the new base classes. Closes gh-27628
This commit is contained in:
parent
b6c2e8de23
commit
b8243e6f84
|
@ -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")))
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 <T> the type of {@link BinaryFormat}
|
||||
*/
|
||||
public abstract class KotlinSerializationBinaryDecoder<T extends BinaryFormat> extends KotlinSerializationSupport<T>
|
||||
implements Decoder<Object> {
|
||||
|
||||
// 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.
|
||||
* <p>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<MimeType> getDecodableMimeTypes() {
|
||||
return supportedMimeTypes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MimeType> getDecodableMimeTypes(ResolvableType targetType) {
|
||||
return supportedMimeTypes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<Object> decode(Publisher<DataBuffer> inputStream, ResolvableType elementType,
|
||||
@Nullable MimeType mimeType,
|
||||
@Nullable Map<String, Object> hints) {
|
||||
|
||||
return Flux.error(new UnsupportedOperationException());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Object> decodeToMono(Publisher<DataBuffer> inputStream, ResolvableType elementType,
|
||||
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
|
||||
return Mono.defer(() -> {
|
||||
KSerializer<Object> 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));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -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 <T> the type of {@link BinaryFormat}
|
||||
*/
|
||||
public abstract class KotlinSerializationBinaryEncoder<T extends BinaryFormat> extends KotlinSerializationSupport<T>
|
||||
implements Encoder<Object> {
|
||||
|
||||
// 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<MimeType> getEncodableMimeTypes() {
|
||||
return supportedMimeTypes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MimeType> getEncodableMimeTypes(ResolvableType elementType) {
|
||||
return supportedMimeTypes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<DataBuffer> encode(Publisher<?> inputStream, DataBufferFactory bufferFactory,
|
||||
ResolvableType elementType,
|
||||
@Nullable MimeType mimeType, @Nullable Map<String, Object> 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<String, Object> hints) {
|
||||
|
||||
KSerializer<Object> 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);
|
||||
}
|
||||
}
|
|
@ -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 <T> the type of {@link StringFormat}
|
||||
*/
|
||||
public abstract class KotlinSerializationStringDecoder<T extends StringFormat> extends KotlinSerializationSupport<T>
|
||||
implements Decoder<Object> {
|
||||
|
||||
// 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.
|
||||
* <p>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<MimeType> getDecodableMimeTypes() {
|
||||
return supportedMimeTypes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MimeType> getDecodableMimeTypes(ResolvableType targetType) {
|
||||
return supportedMimeTypes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<Object> decode(Publisher<DataBuffer> inputStream, ResolvableType elementType,
|
||||
@Nullable MimeType mimeType,
|
||||
@Nullable Map<String, Object> hints) {
|
||||
return Flux.error(new UnsupportedOperationException());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Object> decodeToMono(Publisher<DataBuffer> inputStream, ResolvableType elementType,
|
||||
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
|
||||
|
||||
return Mono.defer(() -> {
|
||||
KSerializer<Object> 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));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -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 <T> the type of {@link StringFormat}
|
||||
*/
|
||||
public abstract class KotlinSerializationStringEncoder<T extends StringFormat> extends KotlinSerializationSupport<T>
|
||||
implements Encoder<Object> {
|
||||
|
||||
// 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<MimeType> getEncodableMimeTypes() {
|
||||
return supportedMimeTypes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MimeType> getEncodableMimeTypes(ResolvableType elementType) {
|
||||
return supportedMimeTypes();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Flux<DataBuffer> encode(Publisher<?> inputStream, DataBufferFactory bufferFactory,
|
||||
ResolvableType elementType,
|
||||
@Nullable MimeType mimeType, @Nullable Map<String, Object> 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<String, Object> hints) {
|
||||
|
||||
KSerializer<Object> 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);
|
||||
}
|
||||
}
|
|
@ -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 <T> the type of {@link SerialFormat}
|
||||
*/
|
||||
public abstract class KotlinSerializationSupport<T extends SerialFormat> {
|
||||
|
||||
private static final Map<Type, KSerializer<Object>> serializerCache = new ConcurrentReferenceHashMap<>();
|
||||
|
||||
|
||||
private final T format;
|
||||
|
||||
private final List<MimeType> 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<MimeType> 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<Object> 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 <a href="https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#open-polymorphism">open polymorphic</a>
|
||||
* 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<Object> serializer(ResolvableType resolvableType) {
|
||||
Type type = resolvableType.getType();
|
||||
KSerializer<Object> 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<String> 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
* <a href="https://github.com/Kotlin/kotlinx.serialization">kotlinx.serialization</a>.
|
||||
*
|
||||
* <p>This decoder can be used to bind {@code @Serializable} Kotlin classes,
|
||||
* <a href="https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#open-polymorphism">open polymorphic serialization</a>
|
||||
* is not supported.
|
||||
* It supports {@code application/cbor}.
|
||||
*
|
||||
* <p>Decoding streams is not supported yet, see
|
||||
* <a href="https://github.com/Kotlin/kotlinx.serialization/issues/1073">kotlinx.serialization/issues/1073</a>
|
||||
* related issue.
|
||||
*
|
||||
* @author Iain Henderson
|
||||
* @since 6.0
|
||||
*/
|
||||
public class KotlinSerializationCborDecoder extends KotlinSerializationBinaryDecoder<Cbor> {
|
||||
|
||||
public KotlinSerializationCborDecoder() {
|
||||
this(Cbor.Default);
|
||||
}
|
||||
|
||||
public KotlinSerializationCborDecoder(Cbor cbor) {
|
||||
super(cbor, MediaType.APPLICATION_CBOR);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
* <a href="https://github.com/Kotlin/kotlinx.serialization">kotlinx.serialization</a>.
|
||||
*
|
||||
* <p>This encoder can be used to bind {@code @Serializable} Kotlin classes,
|
||||
* <a href="https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#open-polymorphism">open polymorphic serialization</a>
|
||||
* is not supported.
|
||||
* It supports {@code application/cbor}.
|
||||
*
|
||||
* @author Iain Henderson
|
||||
* @since 6.0
|
||||
*/
|
||||
public class KotlinSerializationCborEncoder extends KotlinSerializationBinaryEncoder<Cbor> {
|
||||
|
||||
public KotlinSerializationCborEncoder() {
|
||||
this(Cbor.Default);
|
||||
}
|
||||
|
||||
public KotlinSerializationCborEncoder(Cbor cbor) {
|
||||
super(cbor, MediaType.APPLICATION_CBOR);
|
||||
}
|
||||
|
||||
}
|
|
@ -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<Object> {
|
||||
|
||||
private static final Map<Type, KSerializer<Object>> 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<Json> {
|
||||
|
||||
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.
|
||||
* <p>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<Object> decode(Publisher<DataBuffer> inputStream, ResolvableType elementType,
|
||||
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
|
||||
|
||||
return Flux.error(new UnsupportedOperationException());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Object> decodeToMono(Publisher<DataBuffer> inputStream, ResolvableType elementType,
|
||||
@Nullable MimeType mimeType, @Nullable Map<String, Object> 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.
|
||||
* <p>Resolved serializers are cached and cached results are returned on successive calls.
|
||||
* @param type the type to find a serializer for
|
||||
* @return a resolved serializer for the given type or {@code null} if no serializer
|
||||
* supporting the given type can be found
|
||||
*/
|
||||
@Nullable
|
||||
private KSerializer<Object> serializer(Type type) {
|
||||
KSerializer<Object> 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<String> 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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<Object> {
|
||||
|
||||
private static final Map<Type, KSerializer<Object>> 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<Json> {
|
||||
|
||||
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<DataBuffer> encode(Publisher<?> inputStream, DataBufferFactory bufferFactory,
|
||||
ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map<String, Object> 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<String, Object> 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.
|
||||
* <p>Resolved serializers are cached and cached results are returned on successive calls.
|
||||
* @param type the type to find a serializer for
|
||||
* @return a resolved serializer for the given type or {@code null} if no serializer
|
||||
* supporting the given type can be found
|
||||
*/
|
||||
@Nullable
|
||||
private KSerializer<Object> serializer(Type type) {
|
||||
KSerializer<Object> 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<String> 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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
* <a href="https://github.com/Kotlin/kotlinx.serialization">kotlinx.serialization</a>.
|
||||
*
|
||||
* <p>This decoder can be used to bind {@code @Serializable} Kotlin classes,
|
||||
* <a href="https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#open-polymorphism">open polymorphic serialization</a>
|
||||
* is not supported.
|
||||
* It supports {@code application/x-protobuf}, {@code application/octet-stream}, and {@code application/vnd.google.protobuf}.
|
||||
*
|
||||
* <p>Decoding streams is not supported yet, see
|
||||
* <a href="https://github.com/Kotlin/kotlinx.serialization/issues/1073">kotlinx.serialization/issues/1073</a>
|
||||
* related issue.
|
||||
*
|
||||
* @author Iain Henderson
|
||||
* @since 6.0
|
||||
*/
|
||||
public class KotlinSerializationProtobufDecoder extends KotlinSerializationBinaryDecoder<ProtoBuf> {
|
||||
|
||||
public KotlinSerializationProtobufDecoder() {
|
||||
this(ProtoBuf.Default);
|
||||
}
|
||||
|
||||
public KotlinSerializationProtobufDecoder(ProtoBuf protobuf) {
|
||||
super(protobuf, ProtobufCodecSupport.MIME_TYPES);
|
||||
}
|
||||
}
|
|
@ -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
|
||||
* <a href="https://github.com/Kotlin/kotlinx.serialization">kotlinx.serialization</a>.
|
||||
*
|
||||
* <p>This decoder can be used to bind {@code @Serializable} Kotlin classes,
|
||||
* <a href="https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#open-polymorphism">open polymorphic serialization</a>
|
||||
* is not supported.
|
||||
* It supports {@code application/x-protobuf}, {@code application/octet-stream}, and {@code application/vnd.google.protobuf}.
|
||||
*
|
||||
* <p>Decoding streams is not supported yet, see
|
||||
* <a href="https://github.com/Kotlin/kotlinx.serialization/issues/1073">kotlinx.serialization/issues/1073</a>
|
||||
* related issue.
|
||||
*
|
||||
* @author Iain Henderson
|
||||
* @since 6.0
|
||||
*/
|
||||
public class KotlinSerializationProtobufEncoder extends KotlinSerializationBinaryEncoder<ProtoBuf> {
|
||||
|
||||
public KotlinSerializationProtobufEncoder() {
|
||||
this(ProtoBuf.Default);
|
||||
}
|
||||
|
||||
public KotlinSerializationProtobufEncoder(ProtoBuf protobuf) {
|
||||
super(protobuf, ProtobufCodecSupport.MIME_TYPES);
|
||||
}
|
||||
}
|
|
@ -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<MimeType> 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<MimeType> getMimeTypes() {
|
||||
return MIME_TYPES;
|
||||
return Arrays.asList(MIME_TYPES);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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<Message> {
|
||||
|
||||
private static final List<MediaType> streamingMediaTypes = MIME_TYPES
|
||||
.stream()
|
||||
private static final List<MediaType> streamingMediaTypes = Arrays.stream(MIME_TYPES)
|
||||
.map(mimeType -> new MediaType(mimeType.getType(), mimeType.getSubtype(),
|
||||
Collections.singletonMap(DELIMITED_KEY, DELIMITED_VALUE)))
|
||||
.toList();
|
||||
|
|
|
@ -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<Object> 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<Object> 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<HttpMessageWriter<?>> getTypedWriters() {
|
||||
return this.typedWriters;
|
||||
}
|
||||
|
@ -608,9 +682,19 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
|
|||
*/
|
||||
final List<HttpMessageWriter<?>> getBaseObjectWriters() {
|
||||
List<HttpMessageWriter<?>> 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()));
|
||||
}
|
||||
|
|
|
@ -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 <T> the type of {@link SerialFormat}
|
||||
*/
|
||||
public abstract class AbstractKotlinSerializationHttpMessageConverter<T extends SerialFormat> extends AbstractGenericHttpMessageConverter<Object> {
|
||||
|
||||
private static final Map<Type, KSerializer<Object>> 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<Object> 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<Object> 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<Object> 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<Object> 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<Object> 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.
|
||||
* <p>Resolved serializers are cached and cached results are returned on successive calls.
|
||||
* @param type the type to find a serializer for
|
||||
* @return a resolved serializer for the given type, or {@code null}
|
||||
*/
|
||||
@Nullable
|
||||
private KSerializer<Object> serializer(Type type) {
|
||||
KSerializer<Object> 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<String> 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;
|
||||
}
|
||||
}
|
|
@ -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 <T> the type of {@link BinaryFormat}
|
||||
*/
|
||||
public abstract class KotlinSerializationBinaryHttpMessageConverter<T extends BinaryFormat>
|
||||
extends AbstractKotlinSerializationHttpMessageConverter<T> {
|
||||
|
||||
/**
|
||||
* Construct an {@code KotlinSerializationBinaryHttpMessageConverter} with format and supported media types.
|
||||
*/
|
||||
protected KotlinSerializationBinaryHttpMessageConverter(T format, MediaType... supportedMediaTypes) {
|
||||
super(format, supportedMediaTypes);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object readInternal(KSerializer<Object> 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<Object> 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <T> the type of {@link StringFormat}
|
||||
*/
|
||||
public abstract class KotlinSerializationStringHttpMessageConverter<T extends StringFormat>
|
||||
extends AbstractKotlinSerializationHttpMessageConverter<T> {
|
||||
|
||||
|
||||
/**
|
||||
* Construct an {@code KotlinSerializationStringHttpMessageConverter} with format and supported media types.
|
||||
*/
|
||||
protected KotlinSerializationStringHttpMessageConverter(T format, MediaType... supportedMediaTypes) {
|
||||
super(format, supportedMediaTypes);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected Object readInternal(KSerializer<Object> 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<Object> 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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
* <a href="https://github.com/Kotlin/kotlinx.serialization">kotlinx.serialization</a>.
|
||||
*
|
||||
* <p>This converter can be used to bind {@code @Serializable} Kotlin classes,
|
||||
* <a href="https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#open-polymorphism">open polymorphic serialization</a>
|
||||
* is not supported.
|
||||
* It supports {@code application/cbor}.
|
||||
*
|
||||
* @author Iain Henderson
|
||||
* @since 6.0
|
||||
*/
|
||||
public class KotlinSerializationCborHttpMessageConverter extends KotlinSerializationBinaryHttpMessageConverter<Cbor> {
|
||||
public KotlinSerializationCborHttpMessageConverter() {
|
||||
this(Cbor.Default);
|
||||
}
|
||||
|
||||
public KotlinSerializationCborHttpMessageConverter(Cbor cbor) {
|
||||
super(cbor, MediaType.APPLICATION_CBOR);
|
||||
}
|
||||
|
||||
}
|
|
@ -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<Object> {
|
||||
|
||||
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
|
||||
|
||||
private static final Map<Type, KSerializer<Object>> serializerCache = new ConcurrentReferenceHashMap<>();
|
||||
|
||||
private final Json json;
|
||||
|
||||
public class KotlinSerializationJsonHttpMessageConverter extends KotlinSerializationStringHttpMessageConverter<Json> {
|
||||
|
||||
/**
|
||||
* 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<Object> 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<Object> serializer = serializer(clazz);
|
||||
Assert.notNull(serializer, "The serializer should not be null");
|
||||
return decode(serializer, inputMessage);
|
||||
}
|
||||
|
||||
private Object decode(KSerializer<Object> 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<Object> 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<Object> 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.
|
||||
* <p>Resolved serializers are cached and cached results are returned on successive calls.
|
||||
* @param type the type to find a serializer for
|
||||
* @return a resolved serializer for the given type or {@code null} if no serializer
|
||||
* supporting the given type can be found
|
||||
*/
|
||||
@Nullable
|
||||
private KSerializer<Object> serializer(Type type) {
|
||||
KSerializer<Object> 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<String> 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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
* <a href="https://github.com/Kotlin/kotlinx.serialization">kotlinx.serialization</a>.
|
||||
*
|
||||
* <p>This converter can be used to bind {@code @Serializable} Kotlin classes,
|
||||
* <a href="https://github.com/Kotlin/kotlinx.serialization/blob/master/docs/polymorphism.md#open-polymorphism">open polymorphic serialization</a>
|
||||
* 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<ProtoBuf> {
|
||||
|
||||
public KotlinSerializationProtobufHttpMessageConverter() {
|
||||
this(ProtoBuf.Default);
|
||||
}
|
||||
|
||||
public KotlinSerializationProtobufHttpMessageConverter(ProtoBuf protobuf) {
|
||||
super(protobuf, MediaType.APPLICATION_PROTOBUF, MediaType.APPLICATION_OCTET_STREAM,
|
||||
new MediaType("application", "vnd.google.protobuf"));
|
||||
}
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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<DataBuffer> 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<DataBuffer> subscriber = new ZeroDemandSubscriber();
|
||||
flux.subscribe(subscriber); // Assume sync execution (e.g. encoding with Flux.just)..
|
||||
|
|
|
@ -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<ProtobufDecoder> {
|
||||
|
||||
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<ProtobufDecoder>
|
|||
@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
|
||||
|
|
|
@ -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<HttpMessageReader<?>> 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<HttpMessageWriter<?>> 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<HttpMessageReader<?>> 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<HttpMessageWriter<?>> writers =
|
||||
findCodec(clone.getWriters(), MultipartHttpMessageWriter.class).getPartWriters();
|
||||
|
||||
assertThat(writers).hasSize(13);
|
||||
assertThat(writers).hasSize(15);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -259,7 +269,7 @@ public class ClientCodecConfigurerTests {
|
|||
List<HttpMessageWriter<?>> writers =
|
||||
findCodec(clone.getWriters(), MultipartHttpMessageWriter.class).getPartWriters();
|
||||
|
||||
assertThat(writers).hasSize(13);
|
||||
assertThat(writers).hasSize(15);
|
||||
}
|
||||
|
||||
private Decoder<?> getNextDecoder(List<HttpMessageReader<?>> readers) {
|
||||
|
|
|
@ -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<HttpMessageReader<?>> 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<HttpMessageWriter<?>> 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<HttpMessageReader<?>> 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<HttpMessageWriter<?>> 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);
|
||||
|
|
|
@ -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<HttpMessageReader<?>> 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<HttpMessageWriter<?>> 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);
|
||||
|
|
|
@ -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>(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<Any> ->
|
||||
step
|
||||
.expectNext(pojo1)
|
||||
.expectComplete()
|
||||
.verify()
|
||||
}, null, null)
|
||||
}
|
||||
|
||||
private fun byteBuffer(value: Any): Mono<DataBuffer> {
|
||||
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)
|
|
@ -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>(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<DataBuffer?> ->
|
||||
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<DataBuffer?> ->
|
||||
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)
|
||||
|
||||
}
|
|
@ -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>(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<Any> ->
|
||||
step
|
||||
.expectNext(pojo2)
|
||||
.expectComplete()
|
||||
.verify()
|
||||
}, null, null)
|
||||
}
|
||||
|
||||
private fun byteBuffer(value: Any): Mono<DataBuffer> {
|
||||
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)
|
|
@ -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>(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<DataBuffer?> ->
|
||||
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<DataBuffer?> ->
|
||||
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)
|
||||
|
||||
}
|
|
@ -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<String, SerializableBean>>(), Map::class.java, MediaType.APPLICATION_CBOR)).isTrue()
|
||||
assertThat(converter.canRead(List::class.java, MediaType.APPLICATION_CBOR)).isFalse()
|
||||
assertThat(converter.canRead(typeTokenOf<List<SerializableBean>>(), List::class.java, MediaType.APPLICATION_CBOR)).isTrue()
|
||||
assertThat(converter.canRead(Set::class.java, MediaType.APPLICATION_CBOR)).isFalse()
|
||||
assertThat(converter.canRead(typeTokenOf<Set<SerializableBean>>(), Set::class.java, MediaType.APPLICATION_CBOR)).isTrue()
|
||||
|
||||
assertThat(converter.canRead(typeTokenOf<List<Int>>(), List::class.java, MediaType.APPLICATION_CBOR)).isTrue()
|
||||
assertThat(converter.canRead(typeTokenOf<ArrayList<Int>>(), List::class.java, MediaType.APPLICATION_CBOR)).isTrue()
|
||||
assertThat(converter.canRead(typeTokenOf<List<Int>>(), List::class.java, MediaType.APPLICATION_JSON)).isFalse()
|
||||
|
||||
assertThat(converter.canRead(typeTokenOf<Ordered>(), Ordered::class.java, MediaType.APPLICATION_CBOR)).isFalse()
|
||||
assertThat(converter.canRead(typeTokenOf<List<Ordered>>(), 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<String, SerializableBean>>(), Map::class.java, MediaType.APPLICATION_CBOR)).isTrue()
|
||||
assertThat(converter.canWrite(List::class.java, MediaType.APPLICATION_CBOR)).isFalse()
|
||||
assertThat(converter.canWrite(typeTokenOf<List<SerializableBean>>(), List::class.java, MediaType.APPLICATION_CBOR)).isTrue()
|
||||
assertThat(converter.canWrite(Set::class.java, MediaType.APPLICATION_CBOR)).isFalse()
|
||||
assertThat(converter.canWrite(typeTokenOf<Set<SerializableBean>>(), Set::class.java, MediaType.APPLICATION_CBOR)).isTrue()
|
||||
|
||||
assertThat(converter.canWrite(typeTokenOf<List<Int>>(), List::class.java, MediaType.APPLICATION_CBOR)).isTrue()
|
||||
assertThat(converter.canWrite(typeTokenOf<ArrayList<Int>>(), List::class.java, MediaType.APPLICATION_CBOR)).isTrue()
|
||||
assertThat(converter.canWrite(typeTokenOf<List<Int>>(), List::class.java, MediaType.APPLICATION_JSON)).isFalse()
|
||||
|
||||
assertThat(converter.canWrite(typeTokenOf<Ordered>(), 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<SerializableBean>::class.java, inputMessage) as Array<SerializableBean>
|
||||
|
||||
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<List<SerializableBean>>().javaType, null, inputMessage)
|
||||
as List<SerializableBean>
|
||||
|
||||
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<List<SerializableBean>>().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<String>,
|
||||
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<T>
|
||||
|
||||
inline fun <reified T> typeTokenOf(): Type {
|
||||
val base = object : TypeBase<T>() {}
|
||||
val superType = base::class.java.genericSuperclass!!
|
||||
return (superType as ParameterizedType).actualTypeArguments.first()!!
|
||||
}
|
||||
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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<String, SerializableBean>>(), Map::class.java, mimeType)).isTrue()
|
||||
assertThat(converter.canRead(List::class.java, mimeType)).isFalse()
|
||||
assertThat(converter.canRead(typeTokenOf<List<SerializableBean>>(), List::class.java, mimeType)).isTrue()
|
||||
assertThat(converter.canRead(Set::class.java, mimeType)).isFalse()
|
||||
assertThat(converter.canRead(typeTokenOf<Set<SerializableBean>>(), Set::class.java, mimeType)).isTrue()
|
||||
|
||||
assertThat(converter.canRead(typeTokenOf<List<Int>>(), List::class.java, mimeType)).isTrue()
|
||||
assertThat(converter.canRead(typeTokenOf<ArrayList<Int>>(), List::class.java, mimeType)).isTrue()
|
||||
|
||||
assertThat(converter.canRead(typeTokenOf<Ordered>(), Ordered::class.java, mimeType)).isFalse()
|
||||
assertThat(converter.canRead(typeTokenOf<List<Ordered>>(), List::class.java, mimeType)).isFalse()
|
||||
}
|
||||
assertThat(converter.canRead(SerializableBean::class.java, MediaType.APPLICATION_JSON)).isFalse()
|
||||
assertThat(converter.canRead(typeTokenOf<List<Int>>(), 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<String, SerializableBean>>(), Map::class.java, mimeType)).isTrue()
|
||||
assertThat(converter.canWrite(List::class.java, mimeType)).isFalse()
|
||||
assertThat(converter.canWrite(typeTokenOf<List<SerializableBean>>(), List::class.java, mimeType)).isTrue()
|
||||
assertThat(converter.canWrite(Set::class.java, mimeType)).isFalse()
|
||||
assertThat(converter.canWrite(typeTokenOf<Set<SerializableBean>>(), Set::class.java, mimeType)).isTrue()
|
||||
|
||||
assertThat(converter.canWrite(typeTokenOf<List<Int>>(), List::class.java, mimeType)).isTrue()
|
||||
assertThat(converter.canWrite(typeTokenOf<ArrayList<Int>>(), List::class.java, mimeType)).isTrue()
|
||||
|
||||
assertThat(converter.canWrite(typeTokenOf<Ordered>(), Ordered::class.java, mimeType)).isFalse()
|
||||
}
|
||||
|
||||
assertThat(converter.canWrite(SerializableBean::class.java, MediaType.APPLICATION_JSON)).isFalse()
|
||||
assertThat(converter.canWrite(typeTokenOf<List<Int>>(), 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<SerializableBean>::class.java, inputMessage) as Array<SerializableBean>
|
||||
|
||||
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<List<SerializableBean>>().javaType, null, inputMessage)
|
||||
as List<SerializableBean>
|
||||
|
||||
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<List<SerializableBean>>().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<String>,
|
||||
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<T>
|
||||
|
||||
inline fun <reified T> typeTokenOf(): Type {
|
||||
val base = object : TypeBase<T>() {}
|
||||
val superType = base::class.java.genericSuperclass!!
|
||||
return (superType as ParameterizedType).actualTypeArguments.first()!!
|
||||
}
|
||||
|
||||
}
|
|
@ -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"));
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue