Introduce Jackson 3 support for codecs
This commit introduces Jackson 3 variants of the following Jackson 2 classes (and related dependent classes). org.springframework.http.codec.json.Jackson2CodecSupport -> org.springframework.http.codec.JacksonCodecSupport org.springframework.http.codec.json.Jackson2Tokenizer -> org.springframework.http.codec.JacksonTokenizer org.springframework.http.codec.json.Jackson2SmileDecoder -> org.springframework.http.codec.smile.JacksonSmileDecoder org.springframework.http.codec.json.Jackson2SmileEncoder -> org.springframework.http.codec.smile.JacksonSmileEncoder Jackson2CborDecoder -> JacksonCborDecoder Jackson2CborEncoder -> JacksonCborEncoder Jackson2JsonDecoder -> JacksonJsonDecoder Jackson2JsonEncoder -> JacksonJsonEncoder Jackson 3 support is configured if found in the classpath otherwise fallback to Jackson 2. See gh-33798
This commit is contained in:
parent
746679f7a7
commit
5cb2f870d0
|
@ -57,6 +57,12 @@ dependencies {
|
||||||
optional("org.jetbrains.kotlinx:kotlinx-serialization-cbor")
|
optional("org.jetbrains.kotlinx:kotlinx-serialization-cbor")
|
||||||
optional("org.jetbrains.kotlinx:kotlinx-serialization-json")
|
optional("org.jetbrains.kotlinx:kotlinx-serialization-json")
|
||||||
optional("org.jetbrains.kotlinx:kotlinx-serialization-protobuf")
|
optional("org.jetbrains.kotlinx:kotlinx-serialization-protobuf")
|
||||||
|
optional("tools.jackson.core:jackson-databind")
|
||||||
|
optional("tools.jackson.dataformat:jackson-dataformat-smile")
|
||||||
|
optional("tools.jackson.dataformat:jackson-dataformat-cbor")
|
||||||
|
optional("tools.jackson.dataformat:jackson-dataformat-smile")
|
||||||
|
optional("tools.jackson.dataformat:jackson-dataformat-xml")
|
||||||
|
optional("tools.jackson.dataformat:jackson-dataformat-yaml")
|
||||||
optional("com.fasterxml:aalto-xml") // out of order to avoid XML parser override
|
optional("com.fasterxml:aalto-xml") // out of order to avoid XML parser override
|
||||||
testFixturesApi("jakarta.servlet:jakarta.servlet-api")
|
testFixturesApi("jakarta.servlet:jakarta.servlet-api")
|
||||||
testFixturesApi("org.junit.jupiter:junit-jupiter-api")
|
testFixturesApi("org.junit.jupiter:junit-jupiter-api")
|
||||||
|
@ -89,6 +95,7 @@ dependencies {
|
||||||
testImplementation("org.skyscreamer:jsonassert")
|
testImplementation("org.skyscreamer:jsonassert")
|
||||||
testImplementation("org.xmlunit:xmlunit-assertj")
|
testImplementation("org.xmlunit:xmlunit-assertj")
|
||||||
testImplementation("org.xmlunit:xmlunit-matchers")
|
testImplementation("org.xmlunit:xmlunit-matchers")
|
||||||
|
testImplementation("tools.jackson.module:jackson-module-kotlin")
|
||||||
testRuntimeOnly("com.sun.xml.bind:jaxb-core")
|
testRuntimeOnly("com.sun.xml.bind:jaxb-core")
|
||||||
testRuntimeOnly("com.sun.xml.bind:jaxb-impl")
|
testRuntimeOnly("com.sun.xml.bind:jaxb-impl")
|
||||||
testRuntimeOnly("jakarta.json:jakarta.json-api")
|
testRuntimeOnly("jakarta.json:jakarta.json-api")
|
||||||
|
|
|
@ -0,0 +1,298 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2025 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.annotation.Annotation;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
import org.reactivestreams.Publisher;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.util.context.ContextView;
|
||||||
|
import tools.jackson.core.JacksonException;
|
||||||
|
import tools.jackson.core.exc.JacksonIOException;
|
||||||
|
import tools.jackson.databind.DeserializationFeature;
|
||||||
|
import tools.jackson.databind.JavaType;
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
import tools.jackson.databind.ObjectReader;
|
||||||
|
import tools.jackson.databind.cfg.MapperBuilder;
|
||||||
|
import tools.jackson.databind.exc.InvalidDefinitionException;
|
||||||
|
import tools.jackson.databind.util.TokenBuffer;
|
||||||
|
|
||||||
|
import org.springframework.core.MethodParameter;
|
||||||
|
import org.springframework.core.ResolvableType;
|
||||||
|
import org.springframework.core.codec.CodecException;
|
||||||
|
import org.springframework.core.codec.DecodingException;
|
||||||
|
import org.springframework.core.codec.Hints;
|
||||||
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
|
import org.springframework.core.io.buffer.DataBufferLimitException;
|
||||||
|
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||||
|
import org.springframework.core.io.buffer.PooledDataBuffer;
|
||||||
|
import org.springframework.core.log.LogFormatUtils;
|
||||||
|
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||||
|
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.MimeType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract base class for Jackson 3.x decoding, leveraging non-blocking parsing.
|
||||||
|
*
|
||||||
|
* @author Sebastien Deleuze
|
||||||
|
* @since 7.0
|
||||||
|
*/
|
||||||
|
public abstract class AbstractJacksonDecoder extends JacksonCodecSupport implements HttpMessageDecoder<Object> {
|
||||||
|
|
||||||
|
private int maxInMemorySize = 256 * 1024;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new instance with the provided {@link MapperBuilder builder}
|
||||||
|
* customized with the {@link tools.jackson.databind.JacksonModule}s found
|
||||||
|
* by {@link MapperBuilder#findModules(ClassLoader)} and {@link MimeType}s.
|
||||||
|
*/
|
||||||
|
protected AbstractJacksonDecoder(MapperBuilder<?, ?> builder, MimeType... mimeTypes) {
|
||||||
|
super(builder, mimeTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new instance with the provided {@link ObjectMapper} and {@link MimeType}s.
|
||||||
|
*/
|
||||||
|
protected AbstractJacksonDecoder(ObjectMapper mapper, MimeType... mimeTypes) {
|
||||||
|
super(mapper, mimeTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the max number of bytes that can be buffered by this decoder. This
|
||||||
|
* is either the size of the entire input when decoding as a whole, or the
|
||||||
|
* size of one top-level JSON object within a JSON stream. When the limit
|
||||||
|
* is exceeded, {@link DataBufferLimitException} is raised.
|
||||||
|
* <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.maxInMemorySize = byteCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the {@link #setMaxInMemorySize configured} byte count limit.
|
||||||
|
*/
|
||||||
|
public int getMaxInMemorySize() {
|
||||||
|
return this.maxInMemorySize;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) {
|
||||||
|
if (!supportsMimeType(mimeType)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ObjectMapper mapper = selectObjectMapper(elementType, mimeType);
|
||||||
|
if (mapper == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !CharSequence.class.isAssignableFrom(elementType.toClass());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Flux<Object> decode(Publisher<DataBuffer> input, ResolvableType elementType,
|
||||||
|
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
|
||||||
|
|
||||||
|
ObjectMapper mapper = selectObjectMapper(elementType, mimeType);
|
||||||
|
if (mapper == null) {
|
||||||
|
return Flux.error(new IllegalStateException("No ObjectMapper for " + elementType));
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean forceUseOfBigDecimal = mapper.isEnabled(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS);
|
||||||
|
if (BigDecimal.class.equals(elementType.getType())) {
|
||||||
|
forceUseOfBigDecimal = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean tokenizeArrays = (!elementType.isArray() &&
|
||||||
|
!Collection.class.isAssignableFrom(elementType.resolve(Object.class)));
|
||||||
|
|
||||||
|
Flux<DataBuffer> processed = processInput(input, elementType, mimeType, hints);
|
||||||
|
Flux<TokenBuffer> tokens = JacksonTokenizer.tokenize(processed, mapper,
|
||||||
|
tokenizeArrays, forceUseOfBigDecimal, getMaxInMemorySize());
|
||||||
|
|
||||||
|
return Flux.deferContextual(contextView -> {
|
||||||
|
|
||||||
|
Map<String, Object> hintsToUse = contextView.isEmpty() ? hints :
|
||||||
|
Hints.merge(hints, ContextView.class.getName(), contextView);
|
||||||
|
|
||||||
|
ObjectReader reader = createObjectReader(mapper, elementType, hintsToUse);
|
||||||
|
|
||||||
|
return tokens.handle((tokenBuffer, sink) -> {
|
||||||
|
try {
|
||||||
|
Object value = reader.readValue(tokenBuffer.asParser(getObjectMapper()._deserializationContext()));
|
||||||
|
logValue(value, hints);
|
||||||
|
if (value != null) {
|
||||||
|
sink.next(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (JacksonException ex) {
|
||||||
|
sink.error(processException(ex));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.doOnDiscard(PooledDataBuffer.class, DataBufferUtils::release);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Process the input publisher into a flux. Default implementation returns
|
||||||
|
* {@link Flux#from(Publisher)}, but subclasses can choose to customize
|
||||||
|
* this behavior.
|
||||||
|
* @param input the {@code DataBuffer} input stream to process
|
||||||
|
* @param elementType the expected type of elements in the output stream
|
||||||
|
* @param mimeType the MIME type associated with the input stream (optional)
|
||||||
|
* @param hints additional information about how to do encode
|
||||||
|
* @return the processed flux
|
||||||
|
*/
|
||||||
|
protected Flux<DataBuffer> processInput(Publisher<DataBuffer> input, ResolvableType elementType,
|
||||||
|
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
|
||||||
|
|
||||||
|
return Flux.from(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Mono<Object> decodeToMono(Publisher<DataBuffer> input, ResolvableType elementType,
|
||||||
|
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
|
||||||
|
|
||||||
|
return Mono.deferContextual(contextView -> {
|
||||||
|
|
||||||
|
Map<String, Object> hintsToUse = contextView.isEmpty() ? hints :
|
||||||
|
Hints.merge(hints, ContextView.class.getName(), contextView);
|
||||||
|
|
||||||
|
return DataBufferUtils.join(input, this.maxInMemorySize).flatMap(dataBuffer ->
|
||||||
|
Mono.justOrEmpty(decode(dataBuffer, elementType, mimeType, hintsToUse)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Object decode(DataBuffer dataBuffer, ResolvableType targetType,
|
||||||
|
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) throws DecodingException {
|
||||||
|
|
||||||
|
ObjectMapper mapper = selectObjectMapper(targetType, mimeType);
|
||||||
|
if (mapper == null) {
|
||||||
|
throw new IllegalStateException("No ObjectMapper for " + targetType);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ObjectReader objectReader = createObjectReader(mapper, targetType, hints);
|
||||||
|
Object value = objectReader.readValue(dataBuffer.asInputStream());
|
||||||
|
logValue(value, hints);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
catch (JacksonException ex) {
|
||||||
|
throw processException(ex);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
DataBufferUtils.release(dataBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ObjectReader createObjectReader(
|
||||||
|
ObjectMapper mapper, ResolvableType elementType, @Nullable Map<String, Object> hints) {
|
||||||
|
|
||||||
|
Assert.notNull(elementType, "'elementType' must not be null");
|
||||||
|
Class<?> contextClass = getContextClass(elementType);
|
||||||
|
if (contextClass == null && hints != null) {
|
||||||
|
contextClass = getContextClass((ResolvableType) hints.get(ACTUAL_TYPE_HINT));
|
||||||
|
}
|
||||||
|
JavaType javaType = getJavaType(elementType.getType(), contextClass);
|
||||||
|
Class<?> jsonView = (hints != null ? (Class<?>) hints.get(JacksonCodecSupport.JSON_VIEW_HINT) : null);
|
||||||
|
|
||||||
|
ObjectReader objectReader = (jsonView != null ?
|
||||||
|
mapper.readerWithView(jsonView).forType(javaType) :
|
||||||
|
mapper.readerFor(javaType));
|
||||||
|
|
||||||
|
return customizeReader(objectReader, elementType, hints);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subclasses can use this method to customize {@link ObjectReader} used
|
||||||
|
* for reading values.
|
||||||
|
* @param reader the reader instance to customize
|
||||||
|
* @param elementType the target type of element values to read to
|
||||||
|
* @param hints a map with serialization hints;
|
||||||
|
* the Reactor Context, when available, may be accessed under the key
|
||||||
|
* {@code ContextView.class.getName()}
|
||||||
|
* @return the customized {@code ObjectReader} to use
|
||||||
|
*/
|
||||||
|
protected ObjectReader customizeReader(
|
||||||
|
ObjectReader reader, ResolvableType elementType, @Nullable Map<String, Object> hints) {
|
||||||
|
|
||||||
|
return reader;
|
||||||
|
}
|
||||||
|
|
||||||
|
private @Nullable Class<?> getContextClass(@Nullable ResolvableType elementType) {
|
||||||
|
MethodParameter param = (elementType != null ? getParameter(elementType) : null);
|
||||||
|
return (param != null ? param.getContainingClass() : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void logValue(@Nullable Object value, @Nullable Map<String, Object> hints) {
|
||||||
|
if (!Hints.isLoggingSuppressed(hints)) {
|
||||||
|
LogFormatUtils.traceDebug(logger, traceOn -> {
|
||||||
|
String formatted = LogFormatUtils.formatValue(value, !traceOn);
|
||||||
|
return Hints.getLogPrefix(hints) + "Decoded [" + formatted + "]";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private CodecException processException(JacksonException ex) {
|
||||||
|
if (ex instanceof InvalidDefinitionException ide) {
|
||||||
|
JavaType type = ide.getType();
|
||||||
|
return new CodecException("Type definition error: " + type, ex);
|
||||||
|
}
|
||||||
|
if (ex instanceof JacksonIOException) {
|
||||||
|
return new DecodingException("I/O error while parsing input stream", ex);
|
||||||
|
}
|
||||||
|
String originalMessage = ex.getOriginalMessage();
|
||||||
|
return new DecodingException("JSON decoding error: " + originalMessage, ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// HttpMessageDecoder
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getDecodeHints(ResolvableType actualType, ResolvableType elementType,
|
||||||
|
ServerHttpRequest request, ServerHttpResponse response) {
|
||||||
|
|
||||||
|
return getHints(actualType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<MimeType> getDecodableMimeTypes() {
|
||||||
|
return getMimeTypes();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<MimeType> getDecodableMimeTypes(ResolvableType targetType) {
|
||||||
|
return getMimeTypes(targetType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// JacksonCodecSupport
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected <A extends Annotation> @Nullable A getAnnotation(MethodParameter parameter, Class<A> annotType) {
|
||||||
|
return parameter.getParameterAnnotation(annotType);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,444 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2025 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.annotation.Annotation;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
import org.reactivestreams.Publisher;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.util.context.ContextView;
|
||||||
|
import tools.jackson.core.JacksonException;
|
||||||
|
import tools.jackson.core.JsonEncoding;
|
||||||
|
import tools.jackson.core.JsonGenerator;
|
||||||
|
import tools.jackson.core.exc.JacksonIOException;
|
||||||
|
import tools.jackson.core.util.ByteArrayBuilder;
|
||||||
|
import tools.jackson.databind.JavaType;
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
import tools.jackson.databind.ObjectWriter;
|
||||||
|
import tools.jackson.databind.SequenceWriter;
|
||||||
|
import tools.jackson.databind.cfg.MapperBuilder;
|
||||||
|
import tools.jackson.databind.exc.InvalidDefinitionException;
|
||||||
|
import tools.jackson.databind.ser.FilterProvider;
|
||||||
|
|
||||||
|
import org.springframework.core.MethodParameter;
|
||||||
|
import org.springframework.core.ResolvableType;
|
||||||
|
import org.springframework.core.codec.CodecException;
|
||||||
|
import org.springframework.core.codec.EncodingException;
|
||||||
|
import org.springframework.core.codec.Hints;
|
||||||
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
|
import org.springframework.core.io.buffer.DataBufferFactory;
|
||||||
|
import org.springframework.core.log.LogFormatUtils;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.converter.json.MappingJacksonValue;
|
||||||
|
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||||
|
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
import org.springframework.util.MimeType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class providing support methods for Jackson 3.x encoding. For non-streaming use
|
||||||
|
* cases, {@link Flux} elements are collected into a {@link List} before serialization for
|
||||||
|
* performance reasons.
|
||||||
|
*
|
||||||
|
* @author Sebastien Deleuze
|
||||||
|
* @since 7.0
|
||||||
|
*/
|
||||||
|
public abstract class AbstractJacksonEncoder extends JacksonCodecSupport implements HttpMessageEncoder<Object> {
|
||||||
|
|
||||||
|
private static final byte[] NEWLINE_SEPARATOR = {'\n'};
|
||||||
|
|
||||||
|
private static final byte[] EMPTY_BYTES = new byte[0];
|
||||||
|
|
||||||
|
private static final Map<String, JsonEncoding> ENCODINGS;
|
||||||
|
|
||||||
|
static {
|
||||||
|
ENCODINGS = CollectionUtils.newHashMap(JsonEncoding.values().length);
|
||||||
|
for (JsonEncoding encoding : JsonEncoding.values()) {
|
||||||
|
ENCODINGS.put(encoding.getJavaName(), encoding);
|
||||||
|
}
|
||||||
|
ENCODINGS.put("US-ASCII", JsonEncoding.UTF8);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private final List<MediaType> streamingMediaTypes = new ArrayList<>(1);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new instance with the provided {@link MapperBuilder builder}
|
||||||
|
* customized with the {@link tools.jackson.databind.JacksonModule}s found
|
||||||
|
* by {@link MapperBuilder#findModules(ClassLoader)} and {@link MimeType}s.
|
||||||
|
*/
|
||||||
|
protected AbstractJacksonEncoder(MapperBuilder<?, ?> builder, MimeType... mimeTypes) {
|
||||||
|
super(builder, mimeTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new instance with the provided {@link ObjectMapper} and {@link MimeType}s.
|
||||||
|
*/
|
||||||
|
protected AbstractJacksonEncoder(ObjectMapper mapper, MimeType... mimeTypes) {
|
||||||
|
super(mapper, mimeTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure "streaming" media types for which flushing should be performed
|
||||||
|
* automatically vs at the end of the stream.
|
||||||
|
*/
|
||||||
|
public void setStreamingMediaTypes(List<MediaType> mediaTypes) {
|
||||||
|
this.streamingMediaTypes.clear();
|
||||||
|
this.streamingMediaTypes.addAll(mediaTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) {
|
||||||
|
if (!supportsMimeType(mimeType)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (mimeType != null && mimeType.getCharset() != null) {
|
||||||
|
Charset charset = mimeType.getCharset();
|
||||||
|
if (!ENCODINGS.containsKey(charset.name())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.objectMapperRegistrations != null && selectObjectMapper(elementType, mimeType) == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Class<?> clazz = elementType.resolve();
|
||||||
|
if (clazz == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (MappingJacksonValue.class.isAssignableFrom(elementType.resolve(clazz))) {
|
||||||
|
throw new UnsupportedOperationException("MappingJacksonValue is not supported, use hints instead");
|
||||||
|
}
|
||||||
|
return !String.class.isAssignableFrom(elementType.resolve(clazz));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Flux<DataBuffer> encode(Publisher<?> inputStream, DataBufferFactory bufferFactory,
|
||||||
|
ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
|
||||||
|
|
||||||
|
Assert.notNull(inputStream, "'inputStream' must not be null");
|
||||||
|
Assert.notNull(bufferFactory, "'bufferFactory' must not be null");
|
||||||
|
Assert.notNull(elementType, "'elementType' must not be null");
|
||||||
|
|
||||||
|
return Flux.deferContextual(contextView -> {
|
||||||
|
|
||||||
|
Map<String, Object> hintsToUse = contextView.isEmpty() ? hints :
|
||||||
|
Hints.merge(hints, ContextView.class.getName(), contextView);
|
||||||
|
|
||||||
|
if (inputStream instanceof Mono) {
|
||||||
|
return Mono.from(inputStream)
|
||||||
|
.map(value -> encodeValue(value, bufferFactory, elementType, mimeType, hintsToUse))
|
||||||
|
.flux();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
ObjectMapper mapper = selectObjectMapper(elementType, mimeType);
|
||||||
|
if (mapper == null) {
|
||||||
|
throw new IllegalStateException("No ObjectMapper for " + elementType);
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectWriter writer = createObjectWriter(mapper, elementType, mimeType, null, hintsToUse);
|
||||||
|
ByteArrayBuilder byteBuilder = new ByteArrayBuilder(writer.generatorFactory()._getBufferRecycler());
|
||||||
|
JsonEncoding encoding = getJsonEncoding(mimeType);
|
||||||
|
JsonGenerator generator = mapper.createGenerator(byteBuilder, encoding);
|
||||||
|
SequenceWriter sequenceWriter = writer.writeValues(generator);
|
||||||
|
|
||||||
|
byte[] separator = getStreamingMediaTypeSeparator(mimeType);
|
||||||
|
Flux<DataBuffer> dataBufferFlux;
|
||||||
|
|
||||||
|
if (separator != null) {
|
||||||
|
dataBufferFlux = Flux.from(inputStream).map(value -> encodeStreamingValue(
|
||||||
|
value, bufferFactory, hintsToUse, sequenceWriter, byteBuilder, EMPTY_BYTES, separator));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
JsonArrayJoinHelper helper = new JsonArrayJoinHelper();
|
||||||
|
|
||||||
|
// Do not prepend JSON array prefix until first signal is known, onNext vs onError
|
||||||
|
// Keeps response not committed for error handling
|
||||||
|
|
||||||
|
dataBufferFlux = Flux.from(inputStream)
|
||||||
|
.map(value -> {
|
||||||
|
byte[] prefix = helper.getPrefix();
|
||||||
|
byte[] delimiter = helper.getDelimiter();
|
||||||
|
|
||||||
|
DataBuffer dataBuffer = encodeStreamingValue(
|
||||||
|
value, bufferFactory, hintsToUse, sequenceWriter, byteBuilder,
|
||||||
|
delimiter, EMPTY_BYTES);
|
||||||
|
|
||||||
|
return (prefix.length > 0 ?
|
||||||
|
bufferFactory.join(List.of(bufferFactory.wrap(prefix), dataBuffer)) :
|
||||||
|
dataBuffer);
|
||||||
|
})
|
||||||
|
.switchIfEmpty(Mono.fromCallable(() -> bufferFactory.wrap(helper.getPrefix())))
|
||||||
|
.concatWith(Mono.fromCallable(() -> bufferFactory.wrap(helper.getSuffix())));
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataBufferFlux
|
||||||
|
.doOnNext(dataBuffer -> Hints.touchDataBuffer(dataBuffer, hintsToUse, logger))
|
||||||
|
.doAfterTerminate(() -> {
|
||||||
|
try {
|
||||||
|
generator.close();
|
||||||
|
byteBuilder.release();
|
||||||
|
}
|
||||||
|
catch (JacksonIOException ex) {
|
||||||
|
logger.error("Could not close Encoder resources", ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (JacksonIOException ex) {
|
||||||
|
return Flux.error(ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory,
|
||||||
|
ResolvableType valueType, @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
|
||||||
|
|
||||||
|
Class<?> jsonView = null;
|
||||||
|
FilterProvider filters = null;
|
||||||
|
if (hints != null) {
|
||||||
|
jsonView = (Class<?>) hints.get(JSON_VIEW_HINT);
|
||||||
|
filters = (FilterProvider) hints.get(FILTER_PROVIDER_HINT);
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectMapper mapper = selectObjectMapper(valueType, mimeType);
|
||||||
|
if (mapper == null) {
|
||||||
|
throw new IllegalStateException("No ObjectMapper for " + valueType);
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectWriter writer = createObjectWriter(mapper, valueType, mimeType, jsonView, hints);
|
||||||
|
if (filters != null) {
|
||||||
|
writer = writer.with(filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
ByteArrayBuilder byteBuilder = new ByteArrayBuilder(writer.generatorFactory()._getBufferRecycler());
|
||||||
|
try {
|
||||||
|
JsonEncoding encoding = getJsonEncoding(mimeType);
|
||||||
|
|
||||||
|
logValue(hints, value);
|
||||||
|
|
||||||
|
try (JsonGenerator generator = writer.createGenerator(byteBuilder, encoding)) {
|
||||||
|
writer.writeValue(generator, value);
|
||||||
|
generator.flush();
|
||||||
|
}
|
||||||
|
catch (InvalidDefinitionException ex) {
|
||||||
|
throw new CodecException("Type definition error: " + ex.getType(), ex);
|
||||||
|
}
|
||||||
|
catch (JacksonException ex) {
|
||||||
|
throw new EncodingException("JSON encoding error: " + ex.getOriginalMessage(), ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] bytes = byteBuilder.toByteArray();
|
||||||
|
DataBuffer buffer = bufferFactory.allocateBuffer(bytes.length);
|
||||||
|
buffer.write(bytes);
|
||||||
|
Hints.touchDataBuffer(buffer, hints, logger);
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
byteBuilder.release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DataBuffer encodeStreamingValue(
|
||||||
|
Object value, DataBufferFactory bufferFactory, @Nullable Map<String, Object> hints,
|
||||||
|
SequenceWriter sequenceWriter, ByteArrayBuilder byteArrayBuilder,
|
||||||
|
byte[] prefix, byte[] suffix) {
|
||||||
|
|
||||||
|
logValue(hints, value);
|
||||||
|
|
||||||
|
try {
|
||||||
|
sequenceWriter.write(value);
|
||||||
|
sequenceWriter.flush();
|
||||||
|
}
|
||||||
|
catch (InvalidDefinitionException ex) {
|
||||||
|
throw new CodecException("Type definition error: " + ex.getType(), ex);
|
||||||
|
}
|
||||||
|
catch (JacksonException ex) {
|
||||||
|
throw new EncodingException("JSON encoding error: " + ex.getOriginalMessage(), ex);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] bytes = byteArrayBuilder.toByteArray();
|
||||||
|
byteArrayBuilder.reset();
|
||||||
|
|
||||||
|
int offset;
|
||||||
|
int length;
|
||||||
|
if (bytes.length > 0 && bytes[0] == ' ') {
|
||||||
|
// SequenceWriter writes an unnecessary space in between values
|
||||||
|
offset = 1;
|
||||||
|
length = bytes.length - 1;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
offset = 0;
|
||||||
|
length = bytes.length;
|
||||||
|
}
|
||||||
|
DataBuffer buffer = bufferFactory.allocateBuffer(length + prefix.length + suffix.length);
|
||||||
|
if (prefix.length != 0) {
|
||||||
|
buffer.write(prefix);
|
||||||
|
}
|
||||||
|
buffer.write(bytes, offset, length);
|
||||||
|
if (suffix.length != 0) {
|
||||||
|
buffer.write(suffix);
|
||||||
|
}
|
||||||
|
Hints.touchDataBuffer(buffer, hints, logger);
|
||||||
|
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void logValue(@Nullable Map<String, Object> hints, Object value) {
|
||||||
|
if (!Hints.isLoggingSuppressed(hints)) {
|
||||||
|
LogFormatUtils.traceDebug(logger, traceOn -> {
|
||||||
|
String formatted = LogFormatUtils.formatValue(value, !traceOn);
|
||||||
|
return Hints.getLogPrefix(hints) + "Encoding [" + formatted + "]";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ObjectWriter createObjectWriter(
|
||||||
|
ObjectMapper mapper, ResolvableType valueType, @Nullable MimeType mimeType,
|
||||||
|
@Nullable Class<?> jsonView, @Nullable Map<String, Object> hints) {
|
||||||
|
|
||||||
|
JavaType javaType = getJavaType(valueType.getType(), null);
|
||||||
|
if (jsonView == null && hints != null) {
|
||||||
|
jsonView = (Class<?>) hints.get(JacksonCodecSupport.JSON_VIEW_HINT);
|
||||||
|
}
|
||||||
|
ObjectWriter writer = (jsonView != null ? mapper.writerWithView(jsonView) : mapper.writer());
|
||||||
|
if (javaType.isContainerType()) {
|
||||||
|
writer = writer.forType(javaType);
|
||||||
|
}
|
||||||
|
return customizeWriter(writer, mimeType, valueType, hints);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subclasses can use this method to customize the {@link ObjectWriter} used
|
||||||
|
* for writing values.
|
||||||
|
* @param writer the writer instance to customize
|
||||||
|
* @param mimeType the selected MIME type
|
||||||
|
* @param elementType the type of element values to write
|
||||||
|
* @param hints a map with serialization hints; the Reactor Context, when
|
||||||
|
* available, may be accessed under the key
|
||||||
|
* {@code ContextView.class.getName()}
|
||||||
|
* @return the customized {@code ObjectWriter} to use
|
||||||
|
*/
|
||||||
|
protected ObjectWriter customizeWriter(ObjectWriter writer, @Nullable MimeType mimeType,
|
||||||
|
ResolvableType elementType, @Nullable Map<String, Object> hints) {
|
||||||
|
|
||||||
|
return writer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the separator to use for the given mime type.
|
||||||
|
* <p>By default, this method returns new line {@code "\n"} if the given
|
||||||
|
* mime type is one of the configured {@link #setStreamingMediaTypes(List)
|
||||||
|
* streaming} mime types.
|
||||||
|
*/
|
||||||
|
protected byte @Nullable [] getStreamingMediaTypeSeparator(@Nullable MimeType mimeType) {
|
||||||
|
for (MediaType streamingMediaType : this.streamingMediaTypes) {
|
||||||
|
if (streamingMediaType.isCompatibleWith(mimeType)) {
|
||||||
|
return NEWLINE_SEPARATOR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the JSON encoding to use for the given mime type.
|
||||||
|
* @param mimeType the mime type as requested by the caller
|
||||||
|
* @return the JSON encoding to use (never {@code null})
|
||||||
|
*/
|
||||||
|
protected JsonEncoding getJsonEncoding(@Nullable MimeType mimeType) {
|
||||||
|
if (mimeType != null && mimeType.getCharset() != null) {
|
||||||
|
Charset charset = mimeType.getCharset();
|
||||||
|
JsonEncoding result = ENCODINGS.get(charset.name());
|
||||||
|
if (result != null) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return JsonEncoding.UTF8;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// HttpMessageEncoder
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<MimeType> getEncodableMimeTypes() {
|
||||||
|
return getMimeTypes();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<MimeType> getEncodableMimeTypes(ResolvableType elementType) {
|
||||||
|
return getMimeTypes(elementType);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<MediaType> getStreamingMediaTypes() {
|
||||||
|
return Collections.unmodifiableList(this.streamingMediaTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> getEncodeHints(@Nullable ResolvableType actualType, ResolvableType elementType,
|
||||||
|
@Nullable MediaType mediaType, ServerHttpRequest request, ServerHttpResponse response) {
|
||||||
|
|
||||||
|
return (actualType != null ? getHints(actualType) : Hints.none());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// JacksonCodecSupport
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected <A extends Annotation> @Nullable A getAnnotation(MethodParameter parameter, Class<A> annotType) {
|
||||||
|
return parameter.getMethodAnnotation(annotType);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static class JsonArrayJoinHelper {
|
||||||
|
|
||||||
|
private static final byte[] COMMA_SEPARATOR = {','};
|
||||||
|
|
||||||
|
private static final byte[] OPEN_BRACKET = {'['};
|
||||||
|
|
||||||
|
private static final byte[] CLOSE_BRACKET = {']'};
|
||||||
|
|
||||||
|
private boolean firstItemEmitted;
|
||||||
|
|
||||||
|
public byte[] getDelimiter() {
|
||||||
|
if (this.firstItemEmitted) {
|
||||||
|
return COMMA_SEPARATOR;
|
||||||
|
}
|
||||||
|
this.firstItemEmitted = true;
|
||||||
|
return EMPTY_BYTES;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getPrefix() {
|
||||||
|
return (this.firstItemEmitted ? EMPTY_BYTES : OPEN_BRACKET);
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getSuffix() {
|
||||||
|
return CLOSE_BRACKET;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -23,6 +23,8 @@ import org.jspecify.annotations.Nullable;
|
||||||
|
|
||||||
import org.springframework.core.codec.Decoder;
|
import org.springframework.core.codec.Decoder;
|
||||||
import org.springframework.core.codec.Encoder;
|
import org.springframework.core.codec.Encoder;
|
||||||
|
import org.springframework.http.codec.smile.JacksonSmileDecoder;
|
||||||
|
import org.springframework.http.codec.smile.JacksonSmileEncoder;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines a common interface for configuring either client or server HTTP
|
* Defines a common interface for configuring either client or server HTTP
|
||||||
|
@ -109,7 +111,16 @@ public interface CodecConfigurer {
|
||||||
interface DefaultCodecs {
|
interface DefaultCodecs {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Override the default Jackson JSON {@code Decoder}.
|
* Override the default Jackson 3.x JSON {@code Decoder}.
|
||||||
|
* <p>Note that {@link #maxInMemorySize(int)}, if configured, will be
|
||||||
|
* applied to the given decoder.
|
||||||
|
* @param decoder the decoder instance to use
|
||||||
|
* @see org.springframework.http.codec.json.JacksonJsonDecoder
|
||||||
|
*/
|
||||||
|
void jacksonJsonDecoder(Decoder<?> decoder);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override the default Jackson 2.x JSON {@code Decoder}.
|
||||||
* <p>Note that {@link #maxInMemorySize(int)}, if configured, will be
|
* <p>Note that {@link #maxInMemorySize(int)}, if configured, will be
|
||||||
* applied to the given decoder.
|
* applied to the given decoder.
|
||||||
* @param decoder the decoder instance to use
|
* @param decoder the decoder instance to use
|
||||||
|
@ -118,14 +129,30 @@ public interface CodecConfigurer {
|
||||||
void jackson2JsonDecoder(Decoder<?> decoder);
|
void jackson2JsonDecoder(Decoder<?> decoder);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Override the default Jackson JSON {@code Encoder}.
|
* Override the default Jackson 3.x JSON {@code Encoder}.
|
||||||
|
* @param encoder the encoder instance to use
|
||||||
|
* @see org.springframework.http.codec.json.JacksonJsonEncoder
|
||||||
|
*/
|
||||||
|
void jacksonJsonEncoder(Encoder<?> encoder);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override the default Jackson 2.x JSON {@code Encoder}.
|
||||||
* @param encoder the encoder instance to use
|
* @param encoder the encoder instance to use
|
||||||
* @see org.springframework.http.codec.json.Jackson2JsonEncoder
|
* @see org.springframework.http.codec.json.Jackson2JsonEncoder
|
||||||
*/
|
*/
|
||||||
void jackson2JsonEncoder(Encoder<?> encoder);
|
void jackson2JsonEncoder(Encoder<?> encoder);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Override the default Jackson Smile {@code Decoder}.
|
* Override the default Jackson 3.x Smile {@code Decoder}.
|
||||||
|
* <p>Note that {@link #maxInMemorySize(int)}, if configured, will be
|
||||||
|
* applied to the given decoder.
|
||||||
|
* @param decoder the decoder instance to use
|
||||||
|
* @see JacksonSmileDecoder
|
||||||
|
*/
|
||||||
|
void jacksonSmileDecoder(Decoder<?> decoder);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override the default Jackson 2.x Smile {@code Decoder}.
|
||||||
* <p>Note that {@link #maxInMemorySize(int)}, if configured, will be
|
* <p>Note that {@link #maxInMemorySize(int)}, if configured, will be
|
||||||
* applied to the given decoder.
|
* applied to the given decoder.
|
||||||
* @param decoder the decoder instance to use
|
* @param decoder the decoder instance to use
|
||||||
|
@ -134,7 +161,14 @@ public interface CodecConfigurer {
|
||||||
void jackson2SmileDecoder(Decoder<?> decoder);
|
void jackson2SmileDecoder(Decoder<?> decoder);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Override the default Jackson Smile {@code Encoder}.
|
* Override the default Jackson 3.x Smile {@code Encoder}.
|
||||||
|
* @param encoder the encoder instance to use
|
||||||
|
* @see JacksonSmileEncoder
|
||||||
|
*/
|
||||||
|
void jacksonSmileEncoder(Encoder<?> encoder);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Override the default Jackson 2.x Smile {@code Encoder}.
|
||||||
* @param encoder the encoder instance to use
|
* @param encoder the encoder instance to use
|
||||||
* @see org.springframework.http.codec.json.Jackson2SmileEncoder
|
* @see org.springframework.http.codec.json.Jackson2SmileEncoder
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -0,0 +1,275 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2025 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.annotation.Annotation;
|
||||||
|
import java.lang.reflect.Type;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonView;
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
import tools.jackson.databind.JacksonModule;
|
||||||
|
import tools.jackson.databind.JavaType;
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
import tools.jackson.databind.cfg.MapperBuilder;
|
||||||
|
import tools.jackson.databind.ser.FilterProvider;
|
||||||
|
|
||||||
|
import org.springframework.core.GenericTypeResolver;
|
||||||
|
import org.springframework.core.MethodParameter;
|
||||||
|
import org.springframework.core.ResolvableType;
|
||||||
|
import org.springframework.core.codec.Hints;
|
||||||
|
import org.springframework.http.HttpLogging;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ProblemDetail;
|
||||||
|
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||||
|
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||||
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.CollectionUtils;
|
||||||
|
import org.springframework.util.MimeType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base class providing support methods for Jackson 2.x encoding and decoding.
|
||||||
|
*
|
||||||
|
* @author Sebastien Deleuze
|
||||||
|
* @since 7.0
|
||||||
|
*/
|
||||||
|
public abstract class JacksonCodecSupport {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The key for the hint to specify a "JSON View" for encoding or decoding
|
||||||
|
* with the value expected to be a {@link Class}.
|
||||||
|
*/
|
||||||
|
public static final String JSON_VIEW_HINT = JsonView.class.getName();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The key for the hint to specify a {@link FilterProvider}.
|
||||||
|
*/
|
||||||
|
public static final String FILTER_PROVIDER_HINT = FilterProvider.class.getName();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The key for the hint to access the actual ResolvableType passed into
|
||||||
|
* {@link org.springframework.http.codec.HttpMessageReader#read(ResolvableType, ResolvableType, ServerHttpRequest, ServerHttpResponse, Map)}
|
||||||
|
* (server-side only). Currently set when the method argument has generics because
|
||||||
|
* in case of reactive types, use of {@code ResolvableType.getGeneric()} means no
|
||||||
|
* MethodParameter source and no knowledge of the containing class.
|
||||||
|
*/
|
||||||
|
static final String ACTUAL_TYPE_HINT = JacksonCodecSupport.class.getName() + ".actualType";
|
||||||
|
|
||||||
|
private static final String JSON_VIEW_HINT_ERROR =
|
||||||
|
"@JsonView only supported for write hints with exactly 1 class argument: ";
|
||||||
|
|
||||||
|
|
||||||
|
protected final Log logger = HttpLogging.forLogName(getClass());
|
||||||
|
|
||||||
|
private final ObjectMapper defaultObjectMapper;
|
||||||
|
|
||||||
|
protected @Nullable Map<Class<?>, Map<MimeType, ObjectMapper>> objectMapperRegistrations;
|
||||||
|
|
||||||
|
private final List<MimeType> mimeTypes;
|
||||||
|
|
||||||
|
private static volatile @Nullable List<JacksonModule> modules = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new instance with the provided {@link MapperBuilder builder}
|
||||||
|
* customized with the {@link tools.jackson.databind.JacksonModule}s found
|
||||||
|
* by {@link MapperBuilder#findModules(ClassLoader)} and {@link MimeType}s.
|
||||||
|
*/
|
||||||
|
protected JacksonCodecSupport(MapperBuilder<?, ?> builder, MimeType... mimeTypes) {
|
||||||
|
Assert.notNull(builder, "MapperBuilder must not be null");
|
||||||
|
Assert.notEmpty(mimeTypes, "MimeTypes must not be empty");
|
||||||
|
this.defaultObjectMapper = builder.addModules(initModules()).build();
|
||||||
|
this.mimeTypes = List.of(mimeTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new instance with the provided {@link ObjectMapper}
|
||||||
|
* customized with the {@link tools.jackson.databind.JacksonModule}s found
|
||||||
|
* by {@link MapperBuilder#findModules(ClassLoader)} and {@link MimeType}s.
|
||||||
|
*/
|
||||||
|
protected JacksonCodecSupport(ObjectMapper objectMapper, MimeType... mimeTypes) {
|
||||||
|
Assert.notNull(objectMapper, "ObjectMapper must not be null");
|
||||||
|
Assert.notEmpty(mimeTypes, "MimeTypes must not be empty");
|
||||||
|
this.defaultObjectMapper = objectMapper;
|
||||||
|
this.mimeTypes = List.of(mimeTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<JacksonModule> initModules() {
|
||||||
|
if (modules == null) {
|
||||||
|
modules = MapperBuilder.findModules(JacksonCodecSupport.class.getClassLoader());
|
||||||
|
|
||||||
|
}
|
||||||
|
return Objects.requireNonNull(modules);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the {@link ObjectMapper configured} default ObjectMapper.
|
||||||
|
*/
|
||||||
|
public ObjectMapper getObjectMapper() {
|
||||||
|
return this.defaultObjectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure the {@link ObjectMapper} instances to use for the given
|
||||||
|
* {@link Class}. This is useful when you want to deviate from the
|
||||||
|
* {@link #getObjectMapper() default} ObjectMapper or have the
|
||||||
|
* {@code ObjectMapper} vary by {@code MediaType}.
|
||||||
|
* <p><strong>Note:</strong> Use of this method effectively turns off use of
|
||||||
|
* the default {@link #getObjectMapper() ObjectMapper} and supported
|
||||||
|
* {@link #getMimeTypes() MimeTypes} for the given class. Therefore it is
|
||||||
|
* important for the mappings configured here to
|
||||||
|
* {@link MediaType#includes(MediaType) include} every MediaType that must
|
||||||
|
* be supported for the given class.
|
||||||
|
* @param clazz the type of Object to register ObjectMapper instances for
|
||||||
|
* @param registrar a consumer to populate or otherwise update the
|
||||||
|
* MediaType-to-ObjectMapper associations for the given Class
|
||||||
|
*/
|
||||||
|
public void registerObjectMappersForType(Class<?> clazz, Consumer<Map<MimeType, ObjectMapper>> registrar) {
|
||||||
|
if (this.objectMapperRegistrations == null) {
|
||||||
|
this.objectMapperRegistrations = new LinkedHashMap<>();
|
||||||
|
}
|
||||||
|
Map<MimeType, ObjectMapper> registrations =
|
||||||
|
this.objectMapperRegistrations.computeIfAbsent(clazz, c -> new LinkedHashMap<>());
|
||||||
|
registrar.accept(registrations);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return ObjectMapper registrations for the given class, if any.
|
||||||
|
* @param clazz the class to look up for registrations for
|
||||||
|
* @return a map with registered MediaType-to-ObjectMapper registrations,
|
||||||
|
* or empty if in case of no registrations for the given class.
|
||||||
|
*/
|
||||||
|
public @Nullable Map<MimeType, ObjectMapper> getObjectMappersForType(Class<?> clazz) {
|
||||||
|
for (Map.Entry<Class<?>, Map<MimeType, ObjectMapper>> entry : getObjectMapperRegistrations().entrySet()) {
|
||||||
|
if (entry.getKey().isAssignableFrom(clazz)) {
|
||||||
|
return entry.getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Map<Class<?>, Map<MimeType, ObjectMapper>> getObjectMapperRegistrations() {
|
||||||
|
return (this.objectMapperRegistrations != null ? this.objectMapperRegistrations : Collections.emptyMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subclasses should expose this as "decodable" or "encodable" mime types.
|
||||||
|
*/
|
||||||
|
protected List<MimeType> getMimeTypes() {
|
||||||
|
return this.mimeTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<MimeType> getMimeTypes(ResolvableType elementType) {
|
||||||
|
Class<?> elementClass = elementType.toClass();
|
||||||
|
List<MimeType> result = null;
|
||||||
|
for (Map.Entry<Class<?>, Map<MimeType, ObjectMapper>> entry : getObjectMapperRegistrations().entrySet()) {
|
||||||
|
if (entry.getKey().isAssignableFrom(elementClass)) {
|
||||||
|
result = (result != null ? result : new ArrayList<>(entry.getValue().size()));
|
||||||
|
result.addAll(entry.getValue().keySet());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!CollectionUtils.isEmpty(result)) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return (ProblemDetail.class.isAssignableFrom(elementClass) ? getMediaTypesForProblemDetail() : getMimeTypes());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the supported media type(s) for {@link ProblemDetail}.
|
||||||
|
* By default, an empty list, unless overridden in subclasses.
|
||||||
|
*/
|
||||||
|
protected List<MimeType> getMediaTypesForProblemDetail() {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean supportsMimeType(@Nullable MimeType mimeType) {
|
||||||
|
if (mimeType == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (MimeType supportedMimeType : this.mimeTypes) {
|
||||||
|
if (supportedMimeType.isCompatibleWith(mimeType)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected JavaType getJavaType(Type type, @Nullable Class<?> contextClass) {
|
||||||
|
return this.defaultObjectMapper.constructType(GenericTypeResolver.resolveType(type, contextClass));
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Map<String, Object> getHints(ResolvableType resolvableType) {
|
||||||
|
MethodParameter param = getParameter(resolvableType);
|
||||||
|
if (param != null) {
|
||||||
|
Map<String, Object> hints = null;
|
||||||
|
if (resolvableType.hasGenerics()) {
|
||||||
|
hints = new HashMap<>(2);
|
||||||
|
hints.put(ACTUAL_TYPE_HINT, resolvableType);
|
||||||
|
}
|
||||||
|
JsonView annotation = getAnnotation(param, JsonView.class);
|
||||||
|
if (annotation != null) {
|
||||||
|
Class<?>[] classes = annotation.value();
|
||||||
|
Assert.isTrue(classes.length == 1, () -> JSON_VIEW_HINT_ERROR + param);
|
||||||
|
hints = (hints != null ? hints : new HashMap<>(1));
|
||||||
|
hints.put(JSON_VIEW_HINT, classes[0]);
|
||||||
|
}
|
||||||
|
if (hints != null) {
|
||||||
|
return hints;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Hints.none();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected @Nullable MethodParameter getParameter(ResolvableType type) {
|
||||||
|
return (type.getSource() instanceof MethodParameter methodParameter ? methodParameter : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract <A extends Annotation> @Nullable A getAnnotation(MethodParameter parameter, Class<A> annotType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select an ObjectMapper to use, either the main ObjectMapper or another
|
||||||
|
* if the handling for the given Class has been customized through
|
||||||
|
* {@link #registerObjectMappersForType(Class, Consumer)}.
|
||||||
|
*/
|
||||||
|
protected @Nullable ObjectMapper selectObjectMapper(ResolvableType targetType, @Nullable MimeType targetMimeType) {
|
||||||
|
if (targetMimeType == null || CollectionUtils.isEmpty(this.objectMapperRegistrations)) {
|
||||||
|
return this.defaultObjectMapper;
|
||||||
|
}
|
||||||
|
Class<?> targetClass = targetType.toClass();
|
||||||
|
for (Map.Entry<Class<?>, Map<MimeType, ObjectMapper>> typeEntry : getObjectMapperRegistrations().entrySet()) {
|
||||||
|
if (typeEntry.getKey().isAssignableFrom(targetClass)) {
|
||||||
|
for (Map.Entry<MimeType, ObjectMapper> objectMapperEntry : typeEntry.getValue().entrySet()) {
|
||||||
|
if (objectMapperEntry.getKey().includes(targetMimeType)) {
|
||||||
|
return objectMapperEntry.getValue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// No matching registrations
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// No registrations
|
||||||
|
return this.defaultObjectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,241 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2025 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.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import tools.jackson.core.JacksonException;
|
||||||
|
import tools.jackson.core.JsonParser;
|
||||||
|
import tools.jackson.core.JsonToken;
|
||||||
|
import tools.jackson.core.async.ByteArrayFeeder;
|
||||||
|
import tools.jackson.core.async.ByteBufferFeeder;
|
||||||
|
import tools.jackson.core.async.NonBlockingInputFeeder;
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
import tools.jackson.databind.util.TokenBuffer;
|
||||||
|
|
||||||
|
import org.springframework.core.codec.DecodingException;
|
||||||
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
|
import org.springframework.core.io.buffer.DataBufferLimitException;
|
||||||
|
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link Function} to transform a JSON stream of arbitrary size, byte array
|
||||||
|
* chunks into a {@code Flux<TokenBuffer>} where each token buffer is a
|
||||||
|
* well-formed JSON object with Jackson 3.x.
|
||||||
|
*
|
||||||
|
* @author Sebastien Deleuze
|
||||||
|
* @since 7.0
|
||||||
|
*/
|
||||||
|
final class JacksonTokenizer {
|
||||||
|
|
||||||
|
private final JsonParser parser;
|
||||||
|
|
||||||
|
private final NonBlockingInputFeeder inputFeeder;
|
||||||
|
|
||||||
|
private final boolean tokenizeArrayElements;
|
||||||
|
|
||||||
|
private final boolean forceUseOfBigDecimal;
|
||||||
|
|
||||||
|
private final int maxInMemorySize;
|
||||||
|
|
||||||
|
private int objectDepth;
|
||||||
|
|
||||||
|
private int arrayDepth;
|
||||||
|
|
||||||
|
private int byteCount;
|
||||||
|
|
||||||
|
private TokenBuffer tokenBuffer;
|
||||||
|
|
||||||
|
|
||||||
|
private JacksonTokenizer(JsonParser parser, boolean tokenizeArrayElements, boolean forceUseOfBigDecimal, int maxInMemorySize) {
|
||||||
|
this.parser = parser;
|
||||||
|
this.inputFeeder = this.parser.nonBlockingInputFeeder();
|
||||||
|
this.tokenizeArrayElements = tokenizeArrayElements;
|
||||||
|
this.forceUseOfBigDecimal = forceUseOfBigDecimal;
|
||||||
|
this.maxInMemorySize = maxInMemorySize;
|
||||||
|
this.tokenBuffer = createToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private List<TokenBuffer> tokenize(DataBuffer dataBuffer) {
|
||||||
|
try {
|
||||||
|
int bufferSize = dataBuffer.readableByteCount();
|
||||||
|
List<TokenBuffer> tokens = new ArrayList<>();
|
||||||
|
if (this.inputFeeder instanceof ByteBufferFeeder byteBufferFeeder) {
|
||||||
|
try (DataBuffer.ByteBufferIterator iterator = dataBuffer.readableByteBuffers()) {
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
byteBufferFeeder.feedInput(iterator.next());
|
||||||
|
parseTokens(tokens);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (this.inputFeeder instanceof ByteArrayFeeder byteArrayFeeder) {
|
||||||
|
byte[] bytes = new byte[bufferSize];
|
||||||
|
dataBuffer.read(bytes);
|
||||||
|
byteArrayFeeder.feedInput(bytes, 0, bufferSize);
|
||||||
|
parseTokens(tokens);
|
||||||
|
}
|
||||||
|
assertInMemorySize(bufferSize, tokens);
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
catch (JacksonException ex) {
|
||||||
|
throw new DecodingException("JSON decoding error: " + ex.getOriginalMessage(), ex);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
DataBufferUtils.release(dataBuffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Flux<TokenBuffer> endOfInput() {
|
||||||
|
return Flux.defer(() -> {
|
||||||
|
this.inputFeeder.endOfInput();
|
||||||
|
try {
|
||||||
|
List<TokenBuffer> tokens = new ArrayList<>();
|
||||||
|
parseTokens(tokens);
|
||||||
|
return Flux.fromIterable(tokens);
|
||||||
|
}
|
||||||
|
catch (JacksonException ex) {
|
||||||
|
throw new DecodingException("JSON decoding error: " + ex.getOriginalMessage(), ex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void parseTokens(List<TokenBuffer> tokens) {
|
||||||
|
// SPR-16151: Smile data format uses null to separate documents
|
||||||
|
boolean previousNull = false;
|
||||||
|
while (!this.parser.isClosed()) {
|
||||||
|
JsonToken token = this.parser.nextToken();
|
||||||
|
if (token == JsonToken.NOT_AVAILABLE ||
|
||||||
|
token == null && previousNull) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else if (token == null ) { // !previousNull
|
||||||
|
previousNull = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
previousNull = false;
|
||||||
|
}
|
||||||
|
updateDepth(token);
|
||||||
|
if (!this.tokenizeArrayElements) {
|
||||||
|
processTokenNormal(token, tokens);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
processTokenArray(token, tokens);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateDepth(JsonToken token) {
|
||||||
|
switch (token) {
|
||||||
|
case START_OBJECT -> this.objectDepth++;
|
||||||
|
case END_OBJECT -> this.objectDepth--;
|
||||||
|
case START_ARRAY -> this.arrayDepth++;
|
||||||
|
case END_ARRAY -> this.arrayDepth--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processTokenNormal(JsonToken token, List<TokenBuffer> result) {
|
||||||
|
this.tokenBuffer.copyCurrentEvent(this.parser);
|
||||||
|
|
||||||
|
if ((token.isStructEnd() || token.isScalarValue()) && this.objectDepth == 0 && this.arrayDepth == 0) {
|
||||||
|
result.add(this.tokenBuffer);
|
||||||
|
this.tokenBuffer = createToken();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processTokenArray(JsonToken token, List<TokenBuffer> result) {
|
||||||
|
if (!isTopLevelArrayToken(token)) {
|
||||||
|
this.tokenBuffer.copyCurrentEvent(this.parser);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.objectDepth == 0 && (this.arrayDepth == 0 || this.arrayDepth == 1) &&
|
||||||
|
(token == JsonToken.END_OBJECT || token.isScalarValue())) {
|
||||||
|
result.add(this.tokenBuffer);
|
||||||
|
this.tokenBuffer = createToken();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private TokenBuffer createToken() {
|
||||||
|
TokenBuffer tokenBuffer = TokenBuffer.forBuffering(this.parser, this.parser.objectReadContext());
|
||||||
|
tokenBuffer.forceUseOfBigDecimal(this.forceUseOfBigDecimal);
|
||||||
|
return tokenBuffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isTopLevelArrayToken(JsonToken token) {
|
||||||
|
return this.objectDepth == 0 && ((token == JsonToken.START_ARRAY && this.arrayDepth == 1) ||
|
||||||
|
(token == JsonToken.END_ARRAY && this.arrayDepth == 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertInMemorySize(int currentBufferSize, List<TokenBuffer> result) {
|
||||||
|
if (this.maxInMemorySize >= 0) {
|
||||||
|
if (!result.isEmpty()) {
|
||||||
|
this.byteCount = 0;
|
||||||
|
}
|
||||||
|
else if (currentBufferSize > Integer.MAX_VALUE - this.byteCount) {
|
||||||
|
raiseLimitException();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.byteCount += currentBufferSize;
|
||||||
|
if (this.byteCount > this.maxInMemorySize) {
|
||||||
|
raiseLimitException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void raiseLimitException() {
|
||||||
|
throw new DataBufferLimitException(
|
||||||
|
"Exceeded limit on max bytes per JSON object: " + this.maxInMemorySize);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tokenize the given {@code Flux<DataBuffer>} into {@code Flux<TokenBuffer>}.
|
||||||
|
* @param dataBuffers the source data buffers
|
||||||
|
* @param objectMapper the current mapper instance
|
||||||
|
* @param tokenizeArrays if {@code true} and the "top level" JSON object is
|
||||||
|
* an array, each element is returned individually immediately after it is received
|
||||||
|
* @param forceUseOfBigDecimal if {@code true}, any floating point values encountered
|
||||||
|
* in source will use {@link java.math.BigDecimal}
|
||||||
|
* @param maxInMemorySize maximum memory size
|
||||||
|
* @return the resulting token buffers
|
||||||
|
*/
|
||||||
|
public static Flux<TokenBuffer> tokenize(Flux<DataBuffer> dataBuffers,
|
||||||
|
ObjectMapper objectMapper, boolean tokenizeArrays, boolean forceUseOfBigDecimal, int maxInMemorySize) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
JsonParser parser;
|
||||||
|
try {
|
||||||
|
parser = objectMapper.createNonBlockingByteBufferParser();
|
||||||
|
}
|
||||||
|
catch (UnsupportedOperationException ex) {
|
||||||
|
parser = objectMapper.createNonBlockingByteArrayParser();
|
||||||
|
}
|
||||||
|
JacksonTokenizer tokenizer =
|
||||||
|
new JacksonTokenizer(parser, tokenizeArrays, forceUseOfBigDecimal, maxInMemorySize);
|
||||||
|
return dataBuffers.concatMapIterable(tokenizer::tokenize).concatWith(tokenizer.endOfInput());
|
||||||
|
}
|
||||||
|
catch (JacksonException ex) {
|
||||||
|
return Flux.error(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2019 the original author or authors.
|
* Copyright 2002-2025 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -33,7 +33,7 @@ import org.springframework.util.Assert;
|
||||||
import org.springframework.util.MimeType;
|
import org.springframework.util.MimeType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Decode bytes into CBOR and convert to Object's with Jackson.
|
* Decode bytes into CBOR and convert to Object's with Jackson 2.x.
|
||||||
* Stream decoding is not supported yet.
|
* Stream decoding is not supported yet.
|
||||||
*
|
*
|
||||||
* @author Sebastien Deleuze
|
* @author Sebastien Deleuze
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2019 the original author or authors.
|
* Copyright 2002-2025 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -34,7 +34,7 @@ import org.springframework.util.Assert;
|
||||||
import org.springframework.util.MimeType;
|
import org.springframework.util.MimeType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encode from an {@code Object} to bytes of CBOR objects using Jackson.
|
* Encode from an {@code Object} to bytes of CBOR objects using Jackson 2.x.
|
||||||
* Stream encoding is not supported yet.
|
* Stream encoding is not supported yet.
|
||||||
*
|
*
|
||||||
* @author Sebastien Deleuze
|
* @author Sebastien Deleuze
|
||||||
|
|
|
@ -0,0 +1,76 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2025 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 java.util.Map;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
import org.reactivestreams.Publisher;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import tools.jackson.databind.cfg.MapperBuilder;
|
||||||
|
import tools.jackson.dataformat.cbor.CBORMapper;
|
||||||
|
|
||||||
|
import org.springframework.core.ResolvableType;
|
||||||
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.codec.AbstractJacksonDecoder;
|
||||||
|
import org.springframework.util.MimeType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode bytes into CBOR and convert to Object's with Jackson 3.x.
|
||||||
|
* Stream decoding is not supported yet.
|
||||||
|
*
|
||||||
|
* @author Sebastien Deleuze
|
||||||
|
* @since 7.0
|
||||||
|
* @see JacksonCborEncoder
|
||||||
|
* @see <a href="https://github.com/spring-projects/spring-framework/issues/20513">Add CBOR support to WebFlux</a>
|
||||||
|
*/
|
||||||
|
public class JacksonCborDecoder extends AbstractJacksonDecoder {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new instance with a {@link CBORMapper} customized with the
|
||||||
|
* {@link tools.jackson.databind.JacksonModule}s found by
|
||||||
|
* {@link MapperBuilder#findModules(ClassLoader)}.
|
||||||
|
*/
|
||||||
|
public JacksonCborDecoder() {
|
||||||
|
super(CBORMapper.builder(), MediaType.APPLICATION_CBOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new instance with the provided {@link CBORMapper}.
|
||||||
|
*/
|
||||||
|
public JacksonCborDecoder(CBORMapper mapper) {
|
||||||
|
super(mapper, MediaType.APPLICATION_CBOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new instance with the provided {@link CBORMapper} and {@link MimeType}s.
|
||||||
|
* @see CBORMapper#builder()
|
||||||
|
* @see MapperBuilder#findAndAddModules(ClassLoader)
|
||||||
|
*/
|
||||||
|
public JacksonCborDecoder(CBORMapper mapper, MimeType... mimeTypes) {
|
||||||
|
super(mapper, mimeTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Flux<Object> decode(Publisher<DataBuffer> input, ResolvableType elementType, @Nullable MimeType mimeType,
|
||||||
|
@Nullable Map<String, Object> hints) {
|
||||||
|
throw new UnsupportedOperationException("Does not support stream decoding yet");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2025 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 java.util.Map;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
import org.reactivestreams.Publisher;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import tools.jackson.databind.cfg.MapperBuilder;
|
||||||
|
import tools.jackson.dataformat.cbor.CBORMapper;
|
||||||
|
|
||||||
|
import org.springframework.core.ResolvableType;
|
||||||
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
|
import org.springframework.core.io.buffer.DataBufferFactory;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.codec.AbstractJacksonEncoder;
|
||||||
|
import org.springframework.util.MimeType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode from an {@code Object} to bytes of CBOR objects using Jackson 3.x.
|
||||||
|
* Stream encoding is not supported yet.
|
||||||
|
*
|
||||||
|
* @author Sebastien Deleuze
|
||||||
|
* @since 7.0
|
||||||
|
* @see JacksonCborDecoder
|
||||||
|
* @see <a href="https://github.com/spring-projects/spring-framework/issues/20513">Add CBOR support to WebFlux</a>
|
||||||
|
*/
|
||||||
|
public class JacksonCborEncoder extends AbstractJacksonEncoder {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new instance with a {@link CBORMapper} customized with the
|
||||||
|
* {@link tools.jackson.databind.JacksonModule}s found by
|
||||||
|
* {@link MapperBuilder#findModules(ClassLoader)}.
|
||||||
|
*/
|
||||||
|
public JacksonCborEncoder() {
|
||||||
|
super(CBORMapper.builder(), MediaType.APPLICATION_CBOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new instance with the provided {@link CBORMapper}.
|
||||||
|
* @see CBORMapper#builder()
|
||||||
|
* @see MapperBuilder#findAndAddModules(ClassLoader)
|
||||||
|
*/
|
||||||
|
public JacksonCborEncoder(CBORMapper mapper) {
|
||||||
|
super(mapper, MediaType.APPLICATION_CBOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new instance with the provided {@link CBORMapper} and {@link MimeType}s.
|
||||||
|
* @see CBORMapper#builder()
|
||||||
|
* @see MapperBuilder#findAndAddModules(ClassLoader)
|
||||||
|
*/
|
||||||
|
public JacksonCborEncoder(CBORMapper mapper, MimeType... mimeTypes) {
|
||||||
|
super(mapper, mimeTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Flux<DataBuffer> encode(Publisher<?> inputStream, DataBufferFactory bufferFactory, ResolvableType elementType,
|
||||||
|
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
|
||||||
|
throw new UnsupportedOperationException("Does not support stream encoding yet");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -44,7 +44,7 @@ import org.springframework.core.io.buffer.DataBufferUtils;
|
||||||
/**
|
/**
|
||||||
* {@link Function} to transform a JSON stream of arbitrary size, byte array
|
* {@link Function} to transform a JSON stream of arbitrary size, byte array
|
||||||
* chunks into a {@code Flux<TokenBuffer>} where each token buffer is a
|
* chunks into a {@code Flux<TokenBuffer>} where each token buffer is a
|
||||||
* well-formed JSON object.
|
* well-formed JSON object with Jackson 2.x.
|
||||||
*
|
*
|
||||||
* @author Arjen Poutsma
|
* @author Arjen Poutsma
|
||||||
* @author Rossen Stoyanchev
|
* @author Rossen Stoyanchev
|
||||||
|
|
|
@ -0,0 +1,114 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2025 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.json;
|
||||||
|
|
||||||
|
import java.nio.CharBuffer;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
import org.reactivestreams.Publisher;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
import tools.jackson.databind.cfg.MapperBuilder;
|
||||||
|
import tools.jackson.databind.json.JsonMapper;
|
||||||
|
|
||||||
|
import org.springframework.core.ResolvableType;
|
||||||
|
import org.springframework.core.codec.CharBufferDecoder;
|
||||||
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
|
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.codec.AbstractJacksonDecoder;
|
||||||
|
import org.springframework.util.MimeType;
|
||||||
|
import org.springframework.util.MimeTypeUtils;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a byte stream into JSON and convert to Object's with
|
||||||
|
* <a href="https://github.com/FasterXML/jackson">Jackson 3.x</a>
|
||||||
|
* leveraging non-blocking parsing.
|
||||||
|
*
|
||||||
|
* <p>The default constructor loads {@link tools.jackson.databind.JacksonModule}s
|
||||||
|
* found by {@link MapperBuilder#findModules(ClassLoader)}.
|
||||||
|
*
|
||||||
|
* @author Sebastien Deleuze
|
||||||
|
* @since 7.0
|
||||||
|
* @see JacksonJsonEncoder
|
||||||
|
*/
|
||||||
|
public class JacksonJsonDecoder extends AbstractJacksonDecoder {
|
||||||
|
|
||||||
|
private static final CharBufferDecoder CHAR_BUFFER_DECODER = CharBufferDecoder.textPlainOnly(Arrays.asList(",", "\n"), false);
|
||||||
|
|
||||||
|
private static final ResolvableType CHAR_BUFFER_TYPE = ResolvableType.forClass(CharBuffer.class);
|
||||||
|
|
||||||
|
private static final MimeType[] DEFAULT_JSON_MIME_TYPES = new MimeType[] {
|
||||||
|
MediaType.APPLICATION_JSON,
|
||||||
|
new MediaType("application", "*+json"),
|
||||||
|
MediaType.APPLICATION_NDJSON
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new instance with a {@link JsonMapper} customized with the
|
||||||
|
* {@link tools.jackson.databind.JacksonModule}s found by
|
||||||
|
* {@link MapperBuilder#findModules(ClassLoader)}.
|
||||||
|
*/
|
||||||
|
public JacksonJsonDecoder() {
|
||||||
|
super(JsonMapper.builder(), DEFAULT_JSON_MIME_TYPES);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new instance with the provided {@link ObjectMapper}.
|
||||||
|
* @see JsonMapper#builder()
|
||||||
|
* @see MapperBuilder#findModules(ClassLoader)
|
||||||
|
*/
|
||||||
|
public JacksonJsonDecoder(ObjectMapper mapper) {
|
||||||
|
this(mapper, DEFAULT_JSON_MIME_TYPES);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new instance with the provided {@link ObjectMapper} and {@link MimeType}s.
|
||||||
|
* @see JsonMapper#builder()
|
||||||
|
* @see MapperBuilder#findModules(ClassLoader)
|
||||||
|
*/
|
||||||
|
public JacksonJsonDecoder(ObjectMapper mapper, MimeType... mimeTypes) {
|
||||||
|
super(mapper, mimeTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Flux<DataBuffer> processInput(Publisher<DataBuffer> input, ResolvableType elementType,
|
||||||
|
@Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
|
||||||
|
|
||||||
|
Flux<DataBuffer> flux = Flux.from(input);
|
||||||
|
if (mimeType == null) {
|
||||||
|
return flux;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jackson asynchronous parser only supports UTF-8
|
||||||
|
Charset charset = mimeType.getCharset();
|
||||||
|
if (charset == null || StandardCharsets.UTF_8.equals(charset) || StandardCharsets.US_ASCII.equals(charset)) {
|
||||||
|
return flux;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-encode as UTF-8.
|
||||||
|
MimeType textMimeType = new MimeType(MimeTypeUtils.TEXT_PLAIN, charset);
|
||||||
|
Flux<CharBuffer> decoded = CHAR_BUFFER_DECODER.decode(input, CHAR_BUFFER_TYPE, textMimeType, null);
|
||||||
|
return decoded.map(charBuffer -> DefaultDataBufferFactory.sharedInstance.wrap(StandardCharsets.UTF_8.encode(charBuffer)));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,125 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2025 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.json;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import tools.jackson.core.PrettyPrinter;
|
||||||
|
import tools.jackson.core.util.DefaultIndenter;
|
||||||
|
import tools.jackson.core.util.DefaultPrettyPrinter;
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
import tools.jackson.databind.ObjectWriter;
|
||||||
|
import tools.jackson.databind.SerializationFeature;
|
||||||
|
import tools.jackson.databind.cfg.MapperBuilder;
|
||||||
|
import tools.jackson.databind.json.JsonMapper;
|
||||||
|
|
||||||
|
import org.springframework.core.ResolvableType;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ProblemDetail;
|
||||||
|
import org.springframework.http.codec.AbstractJacksonEncoder;
|
||||||
|
import org.springframework.http.converter.json.ProblemDetailJacksonMixin;
|
||||||
|
import org.springframework.util.MimeType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode from an {@code Object} stream to a byte stream of JSON objects using
|
||||||
|
* <a href="https://github.com/FasterXML/jackson">Jackson 3.x</a>. For non-streaming
|
||||||
|
* use cases, {@link Flux} elements are collected into a {@link List} before
|
||||||
|
* serialization for performance reason.
|
||||||
|
*
|
||||||
|
* <p>The default constructor loads {@link tools.jackson.databind.JacksonModule}s
|
||||||
|
* found by {@link MapperBuilder#findModules(ClassLoader)}.
|
||||||
|
*
|
||||||
|
* @author Sebastien Deleuze
|
||||||
|
* @since 7.0
|
||||||
|
* @see JacksonJsonDecoder
|
||||||
|
*/
|
||||||
|
public class JacksonJsonEncoder extends AbstractJacksonEncoder {
|
||||||
|
|
||||||
|
private static final List<MimeType> problemDetailMimeTypes =
|
||||||
|
Collections.singletonList(MediaType.APPLICATION_PROBLEM_JSON);
|
||||||
|
|
||||||
|
private static final MimeType[] DEFAULT_JSON_MIME_TYPES = new MimeType[] {
|
||||||
|
MediaType.APPLICATION_JSON,
|
||||||
|
new MediaType("application", "*+json"),
|
||||||
|
MediaType.APPLICATION_NDJSON
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
private final @Nullable PrettyPrinter ssePrettyPrinter;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new instance with a {@link JsonMapper} customized with the
|
||||||
|
* {@link tools.jackson.databind.JacksonModule}s found by
|
||||||
|
* {@link MapperBuilder#findModules(ClassLoader)} and
|
||||||
|
* {@link ProblemDetailJacksonMixin}.
|
||||||
|
*/
|
||||||
|
public JacksonJsonEncoder() {
|
||||||
|
super(JsonMapper.builder().addMixIn(ProblemDetail.class, ProblemDetailJacksonMixin.class),
|
||||||
|
DEFAULT_JSON_MIME_TYPES);
|
||||||
|
setStreamingMediaTypes(List.of(MediaType.APPLICATION_NDJSON));
|
||||||
|
this.ssePrettyPrinter = initSsePrettyPrinter();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new instance with the provided {@link ObjectMapper}.
|
||||||
|
* @see JsonMapper#builder()
|
||||||
|
* @see MapperBuilder#findModules(ClassLoader)
|
||||||
|
*/
|
||||||
|
public JacksonJsonEncoder(ObjectMapper mapper) {
|
||||||
|
this(mapper, DEFAULT_JSON_MIME_TYPES);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new instance with the provided {@link ObjectMapper} and
|
||||||
|
* {@link MimeType}s.
|
||||||
|
* @see JsonMapper#builder()
|
||||||
|
* @see MapperBuilder#findModules(ClassLoader)
|
||||||
|
*/
|
||||||
|
public JacksonJsonEncoder(ObjectMapper mapper, MimeType... mimeTypes) {
|
||||||
|
super(mapper, mimeTypes);
|
||||||
|
setStreamingMediaTypes(List.of(MediaType.APPLICATION_NDJSON));
|
||||||
|
this.ssePrettyPrinter = initSsePrettyPrinter();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PrettyPrinter initSsePrettyPrinter() {
|
||||||
|
DefaultPrettyPrinter printer = new DefaultPrettyPrinter();
|
||||||
|
printer.indentObjectsWith(new DefaultIndenter(" ", "\ndata:"));
|
||||||
|
return printer;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<MimeType> getMediaTypesForProblemDetail() {
|
||||||
|
return problemDetailMimeTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ObjectWriter customizeWriter(ObjectWriter writer, @Nullable MimeType mimeType,
|
||||||
|
ResolvableType elementType, @Nullable Map<String, Object> hints) {
|
||||||
|
|
||||||
|
return (this.ssePrettyPrinter != null &&
|
||||||
|
MediaType.TEXT_EVENT_STREAM.isCompatibleWith(mimeType) &&
|
||||||
|
writer.getConfig().isEnabled(SerializationFeature.INDENT_OUTPUT) ?
|
||||||
|
writer.with(this.ssePrettyPrinter) : writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,69 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2025 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.smile;
|
||||||
|
|
||||||
|
import tools.jackson.databind.cfg.MapperBuilder;
|
||||||
|
import tools.jackson.dataformat.smile.SmileMapper;
|
||||||
|
|
||||||
|
import org.springframework.http.codec.AbstractJacksonDecoder;
|
||||||
|
import org.springframework.util.MimeType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode a byte stream into Smile and convert to Object's with Jackson 3.x,
|
||||||
|
* leveraging non-blocking parsing.
|
||||||
|
*
|
||||||
|
* <p>The default constructor loads {@link tools.jackson.databind.JacksonModule}s
|
||||||
|
* found by {@link MapperBuilder#findModules(ClassLoader)}.
|
||||||
|
*
|
||||||
|
* @author Sebastien Deleuze
|
||||||
|
* @since 7.0
|
||||||
|
* @see JacksonSmileEncoder
|
||||||
|
*/
|
||||||
|
public class JacksonSmileDecoder extends AbstractJacksonDecoder {
|
||||||
|
|
||||||
|
private static final MimeType[] DEFAULT_SMILE_MIME_TYPES = new MimeType[] {
|
||||||
|
new MimeType("application", "x-jackson-smile"),
|
||||||
|
new MimeType("application", "*+x-jackson-smile")};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new instance with a {@link SmileMapper} customized with the
|
||||||
|
* {@link tools.jackson.databind.JacksonModule}s found by
|
||||||
|
* {@link MapperBuilder#findModules(ClassLoader)}.
|
||||||
|
*/
|
||||||
|
public JacksonSmileDecoder() {
|
||||||
|
super(SmileMapper.builder(), DEFAULT_SMILE_MIME_TYPES);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new instance with the provided {@link SmileMapper}.
|
||||||
|
* @see SmileMapper#builder()
|
||||||
|
* @see MapperBuilder#findAndAddModules(ClassLoader)
|
||||||
|
*/
|
||||||
|
public JacksonSmileDecoder(SmileMapper mapper) {
|
||||||
|
this(mapper, DEFAULT_SMILE_MIME_TYPES);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new instance with the provided {@link SmileMapper} and {@link MimeType}s.
|
||||||
|
* @see SmileMapper#builder()
|
||||||
|
* @see MapperBuilder#findAndAddModules(ClassLoader)
|
||||||
|
*/
|
||||||
|
public JacksonSmileDecoder(SmileMapper mapper, MimeType... mimeTypes) {
|
||||||
|
super(mapper, mimeTypes);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,101 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2025 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.smile;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.Nullable;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import tools.jackson.databind.cfg.MapperBuilder;
|
||||||
|
import tools.jackson.dataformat.smile.SmileMapper;
|
||||||
|
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.codec.AbstractJacksonEncoder;
|
||||||
|
import org.springframework.util.MimeType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode from an {@code Object} stream to a byte stream of Smile objects using Jackson 3.x.
|
||||||
|
* For non-streaming use cases, {@link Flux} elements are collected into a {@link List}
|
||||||
|
* before serialization for performance reason.
|
||||||
|
*
|
||||||
|
* <p>The default constructor loads {@link tools.jackson.databind.JacksonModule}s
|
||||||
|
* found by {@link MapperBuilder#findModules(ClassLoader)}.
|
||||||
|
*
|
||||||
|
* @author Sebastien Deleuze
|
||||||
|
* @since 7.0
|
||||||
|
* @see JacksonSmileDecoder
|
||||||
|
*/
|
||||||
|
public class JacksonSmileEncoder extends AbstractJacksonEncoder {
|
||||||
|
|
||||||
|
private static final MimeType[] DEFAULT_SMILE_MIME_TYPES = new MimeType[] {
|
||||||
|
new MimeType("application", "x-jackson-smile"),
|
||||||
|
new MimeType("application", "*+x-jackson-smile")};
|
||||||
|
|
||||||
|
private static final MediaType DEFAULT_SMILE_STREAMING_MEDIA_TYPE =
|
||||||
|
new MediaType("application", "stream+x-jackson-smile");
|
||||||
|
|
||||||
|
private static final byte[] STREAM_SEPARATOR = new byte[0];
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new instance with a {@link SmileMapper} customized with the
|
||||||
|
* {@link tools.jackson.databind.JacksonModule}s found by
|
||||||
|
* {@link MapperBuilder#findModules(ClassLoader)}.
|
||||||
|
*/
|
||||||
|
public JacksonSmileEncoder() {
|
||||||
|
super(SmileMapper.builder(), DEFAULT_SMILE_MIME_TYPES);
|
||||||
|
setStreamingMediaTypes(Collections.singletonList(DEFAULT_SMILE_STREAMING_MEDIA_TYPE));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new instance with the provided {@link SmileMapper}.
|
||||||
|
* @see SmileMapper#builder()
|
||||||
|
* @see MapperBuilder#findAndAddModules(ClassLoader)
|
||||||
|
*/
|
||||||
|
public JacksonSmileEncoder(SmileMapper mapper) {
|
||||||
|
super(mapper, DEFAULT_SMILE_MIME_TYPES);
|
||||||
|
setStreamingMediaTypes(Collections.singletonList(DEFAULT_SMILE_STREAMING_MEDIA_TYPE));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a new instance with the provided {@link SmileMapper} and {@link MimeType}s.
|
||||||
|
* @see SmileMapper#builder()
|
||||||
|
* @see MapperBuilder#findAndAddModules(ClassLoader)
|
||||||
|
*/
|
||||||
|
public JacksonSmileEncoder(SmileMapper mapper, MimeType... mimeTypes) {
|
||||||
|
super(mapper, mimeTypes);
|
||||||
|
setStreamingMediaTypes(Collections.singletonList(DEFAULT_SMILE_STREAMING_MEDIA_TYPE));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the separator to use for the given mime type.
|
||||||
|
* <p>By default, this method returns a single byte 0 if the given
|
||||||
|
* mime type is one of the configured {@link #setStreamingMediaTypes(List)
|
||||||
|
* streaming} mime types.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected byte @Nullable [] getStreamingMediaTypeSeparator(@Nullable MimeType mimeType) {
|
||||||
|
for (MediaType streamingMediaType : getStreamingMediaTypes()) {
|
||||||
|
if (streamingMediaType.isCompatibleWith(mimeType)) {
|
||||||
|
return STREAM_SEPARATOR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
/**
|
||||||
|
* Provides an encoder and a decoder for the Smile data format ("binary JSON").
|
||||||
|
*/
|
||||||
|
@NullMarked
|
||||||
|
package org.springframework.http.codec.smile;
|
||||||
|
|
||||||
|
import org.jspecify.annotations.NullMarked;
|
|
@ -39,6 +39,7 @@ import org.springframework.core.codec.NettyByteBufDecoder;
|
||||||
import org.springframework.core.codec.NettyByteBufEncoder;
|
import org.springframework.core.codec.NettyByteBufEncoder;
|
||||||
import org.springframework.core.codec.ResourceDecoder;
|
import org.springframework.core.codec.ResourceDecoder;
|
||||||
import org.springframework.core.codec.StringDecoder;
|
import org.springframework.core.codec.StringDecoder;
|
||||||
|
import org.springframework.http.codec.AbstractJacksonDecoder;
|
||||||
import org.springframework.http.codec.CodecConfigurer;
|
import org.springframework.http.codec.CodecConfigurer;
|
||||||
import org.springframework.http.codec.DecoderHttpMessageReader;
|
import org.springframework.http.codec.DecoderHttpMessageReader;
|
||||||
import org.springframework.http.codec.EncoderHttpMessageWriter;
|
import org.springframework.http.codec.EncoderHttpMessageWriter;
|
||||||
|
@ -57,6 +58,8 @@ import org.springframework.http.codec.json.Jackson2JsonDecoder;
|
||||||
import org.springframework.http.codec.json.Jackson2JsonEncoder;
|
import org.springframework.http.codec.json.Jackson2JsonEncoder;
|
||||||
import org.springframework.http.codec.json.Jackson2SmileDecoder;
|
import org.springframework.http.codec.json.Jackson2SmileDecoder;
|
||||||
import org.springframework.http.codec.json.Jackson2SmileEncoder;
|
import org.springframework.http.codec.json.Jackson2SmileEncoder;
|
||||||
|
import org.springframework.http.codec.json.JacksonJsonDecoder;
|
||||||
|
import org.springframework.http.codec.json.JacksonJsonEncoder;
|
||||||
import org.springframework.http.codec.json.KotlinSerializationJsonDecoder;
|
import org.springframework.http.codec.json.KotlinSerializationJsonDecoder;
|
||||||
import org.springframework.http.codec.json.KotlinSerializationJsonEncoder;
|
import org.springframework.http.codec.json.KotlinSerializationJsonEncoder;
|
||||||
import org.springframework.http.codec.multipart.DefaultPartHttpMessageReader;
|
import org.springframework.http.codec.multipart.DefaultPartHttpMessageReader;
|
||||||
|
@ -70,6 +73,8 @@ import org.springframework.http.codec.protobuf.KotlinSerializationProtobufEncode
|
||||||
import org.springframework.http.codec.protobuf.ProtobufDecoder;
|
import org.springframework.http.codec.protobuf.ProtobufDecoder;
|
||||||
import org.springframework.http.codec.protobuf.ProtobufEncoder;
|
import org.springframework.http.codec.protobuf.ProtobufEncoder;
|
||||||
import org.springframework.http.codec.protobuf.ProtobufHttpMessageWriter;
|
import org.springframework.http.codec.protobuf.ProtobufHttpMessageWriter;
|
||||||
|
import org.springframework.http.codec.smile.JacksonSmileDecoder;
|
||||||
|
import org.springframework.http.codec.smile.JacksonSmileEncoder;
|
||||||
import org.springframework.http.codec.xml.Jaxb2XmlDecoder;
|
import org.springframework.http.codec.xml.Jaxb2XmlDecoder;
|
||||||
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
|
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
|
||||||
import org.springframework.util.ClassUtils;
|
import org.springframework.util.ClassUtils;
|
||||||
|
@ -84,8 +89,12 @@ import org.springframework.util.ObjectUtils;
|
||||||
*/
|
*/
|
||||||
class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigurer.DefaultCodecConfig {
|
class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigurer.DefaultCodecConfig {
|
||||||
|
|
||||||
|
static final boolean jacksonPresent;
|
||||||
|
|
||||||
static final boolean jackson2Present;
|
static final boolean jackson2Present;
|
||||||
|
|
||||||
|
private static final boolean jacksonSmilePresent;
|
||||||
|
|
||||||
private static final boolean jackson2SmilePresent;
|
private static final boolean jackson2SmilePresent;
|
||||||
|
|
||||||
private static final boolean jaxb2Present;
|
private static final boolean jaxb2Present;
|
||||||
|
@ -102,9 +111,11 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
|
||||||
|
|
||||||
static {
|
static {
|
||||||
ClassLoader classLoader = BaseCodecConfigurer.class.getClassLoader();
|
ClassLoader classLoader = BaseCodecConfigurer.class.getClassLoader();
|
||||||
|
jacksonPresent = ClassUtils.isPresent("tools.jackson.databind.ObjectMapper", classLoader);
|
||||||
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
|
jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader) &&
|
||||||
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
|
ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
|
||||||
jackson2SmilePresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
|
jacksonSmilePresent = jacksonPresent && ClassUtils.isPresent("tools.jackson.dataformat.smile.SmileMapper", classLoader);
|
||||||
|
jackson2SmilePresent = jackson2Present && ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
|
||||||
jaxb2Present = ClassUtils.isPresent("jakarta.xml.bind.Binder", classLoader);
|
jaxb2Present = ClassUtils.isPresent("jakarta.xml.bind.Binder", classLoader);
|
||||||
protobufPresent = ClassUtils.isPresent("com.google.protobuf.Message", classLoader);
|
protobufPresent = ClassUtils.isPresent("com.google.protobuf.Message", classLoader);
|
||||||
nettyByteBufPresent = ClassUtils.isPresent("io.netty.buffer.ByteBuf", classLoader);
|
nettyByteBufPresent = ClassUtils.isPresent("io.netty.buffer.ByteBuf", classLoader);
|
||||||
|
@ -114,12 +125,20 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private @Nullable Decoder<?> jacksonJsonDecoder;
|
||||||
|
|
||||||
private @Nullable Decoder<?> jackson2JsonDecoder;
|
private @Nullable Decoder<?> jackson2JsonDecoder;
|
||||||
|
|
||||||
|
private @Nullable Encoder<?> jacksonJsonEncoder;
|
||||||
|
|
||||||
private @Nullable Encoder<?> jackson2JsonEncoder;
|
private @Nullable Encoder<?> jackson2JsonEncoder;
|
||||||
|
|
||||||
|
private @Nullable Encoder<?> jacksonSmileEncoder;
|
||||||
|
|
||||||
private @Nullable Encoder<?> jackson2SmileEncoder;
|
private @Nullable Encoder<?> jackson2SmileEncoder;
|
||||||
|
|
||||||
|
private @Nullable Decoder<?> jacksonSmileDecoder;
|
||||||
|
|
||||||
private @Nullable Decoder<?> jackson2SmileDecoder;
|
private @Nullable Decoder<?> jackson2SmileDecoder;
|
||||||
|
|
||||||
private @Nullable Decoder<?> protobufDecoder;
|
private @Nullable Decoder<?> protobufDecoder;
|
||||||
|
@ -195,9 +214,13 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
|
||||||
* Create a deep copy of the given {@link BaseDefaultCodecs}.
|
* Create a deep copy of the given {@link BaseDefaultCodecs}.
|
||||||
*/
|
*/
|
||||||
protected BaseDefaultCodecs(BaseDefaultCodecs other) {
|
protected BaseDefaultCodecs(BaseDefaultCodecs other) {
|
||||||
|
this.jacksonJsonDecoder = other.jacksonJsonDecoder;
|
||||||
this.jackson2JsonDecoder = other.jackson2JsonDecoder;
|
this.jackson2JsonDecoder = other.jackson2JsonDecoder;
|
||||||
|
this.jacksonJsonEncoder = other.jacksonJsonEncoder;
|
||||||
this.jackson2JsonEncoder = other.jackson2JsonEncoder;
|
this.jackson2JsonEncoder = other.jackson2JsonEncoder;
|
||||||
|
this.jacksonSmileDecoder = other.jacksonSmileDecoder;
|
||||||
this.jackson2SmileDecoder = other.jackson2SmileDecoder;
|
this.jackson2SmileDecoder = other.jackson2SmileDecoder;
|
||||||
|
this.jacksonSmileEncoder = other.jacksonSmileEncoder;
|
||||||
this.jackson2SmileEncoder = other.jackson2SmileEncoder;
|
this.jackson2SmileEncoder = other.jackson2SmileEncoder;
|
||||||
this.protobufDecoder = other.protobufDecoder;
|
this.protobufDecoder = other.protobufDecoder;
|
||||||
this.protobufEncoder = other.protobufEncoder;
|
this.protobufEncoder = other.protobufEncoder;
|
||||||
|
@ -222,12 +245,25 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
|
||||||
this.objectWriters.addAll(other.objectWriters);
|
this.objectWriters.addAll(other.objectWriters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void jacksonJsonDecoder(Decoder<?> decoder) {
|
||||||
|
this.jacksonJsonDecoder = decoder;
|
||||||
|
initObjectReaders();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void jackson2JsonDecoder(Decoder<?> decoder) {
|
public void jackson2JsonDecoder(Decoder<?> decoder) {
|
||||||
this.jackson2JsonDecoder = decoder;
|
this.jackson2JsonDecoder = decoder;
|
||||||
initObjectReaders();
|
initObjectReaders();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void jacksonJsonEncoder(Encoder<?> encoder) {
|
||||||
|
this.jacksonJsonEncoder = encoder;
|
||||||
|
initObjectWriters();
|
||||||
|
initTypedWriters();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void jackson2JsonEncoder(Encoder<?> encoder) {
|
public void jackson2JsonEncoder(Encoder<?> encoder) {
|
||||||
this.jackson2JsonEncoder = encoder;
|
this.jackson2JsonEncoder = encoder;
|
||||||
|
@ -235,12 +271,25 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
|
||||||
initTypedWriters();
|
initTypedWriters();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void jacksonSmileDecoder(Decoder<?> decoder) {
|
||||||
|
this.jacksonSmileDecoder = decoder;
|
||||||
|
initObjectReaders();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void jackson2SmileDecoder(Decoder<?> decoder) {
|
public void jackson2SmileDecoder(Decoder<?> decoder) {
|
||||||
this.jackson2SmileDecoder = decoder;
|
this.jackson2SmileDecoder = decoder;
|
||||||
initObjectReaders();
|
initObjectReaders();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void jacksonSmileEncoder(Encoder<?> encoder) {
|
||||||
|
this.jacksonSmileEncoder = encoder;
|
||||||
|
initObjectWriters();
|
||||||
|
initTypedWriters();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void jackson2SmileEncoder(Encoder<?> encoder) {
|
public void jackson2SmileEncoder(Encoder<?> encoder) {
|
||||||
this.jackson2SmileEncoder = encoder;
|
this.jackson2SmileEncoder = encoder;
|
||||||
|
@ -476,6 +525,11 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
|
||||||
kotlinSerializationProtobufDec.setMaxInMemorySize(size);
|
kotlinSerializationProtobufDec.setMaxInMemorySize(size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (jacksonPresent) {
|
||||||
|
if (codec instanceof AbstractJacksonDecoder abstractJacksonDecoder) {
|
||||||
|
abstractJacksonDecoder.setMaxInMemorySize(size);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (jackson2Present) {
|
if (jackson2Present) {
|
||||||
if (codec instanceof AbstractJackson2Decoder abstractJackson2Decoder) {
|
if (codec instanceof AbstractJackson2Decoder abstractJackson2Decoder) {
|
||||||
abstractJackson2Decoder.setMaxInMemorySize(size);
|
abstractJackson2Decoder.setMaxInMemorySize(size);
|
||||||
|
@ -574,13 +628,20 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
|
||||||
(KotlinSerializationProtobufDecoder) this.kotlinSerializationProtobufDecoder :
|
(KotlinSerializationProtobufDecoder) this.kotlinSerializationProtobufDecoder :
|
||||||
new KotlinSerializationProtobufDecoder()));
|
new KotlinSerializationProtobufDecoder()));
|
||||||
}
|
}
|
||||||
if (jackson2Present) {
|
if (jacksonPresent) {
|
||||||
|
addCodec(this.objectReaders, new DecoderHttpMessageReader<>(getJacksonJsonDecoder()));
|
||||||
|
}
|
||||||
|
else if (jackson2Present) {
|
||||||
addCodec(this.objectReaders, new DecoderHttpMessageReader<>(getJackson2JsonDecoder()));
|
addCodec(this.objectReaders, new DecoderHttpMessageReader<>(getJackson2JsonDecoder()));
|
||||||
}
|
}
|
||||||
else if (kotlinSerializationJsonPresent) {
|
else if (kotlinSerializationJsonPresent) {
|
||||||
addCodec(this.objectReaders, new DecoderHttpMessageReader<>(getKotlinSerializationJsonDecoder()));
|
addCodec(this.objectReaders, new DecoderHttpMessageReader<>(getKotlinSerializationJsonDecoder()));
|
||||||
}
|
}
|
||||||
if (jackson2SmilePresent) {
|
if (jacksonSmilePresent) {
|
||||||
|
addCodec(this.objectReaders, new DecoderHttpMessageReader<>(this.jacksonSmileDecoder != null ?
|
||||||
|
(JacksonSmileDecoder) this.jacksonSmileDecoder : new JacksonSmileDecoder()));
|
||||||
|
}
|
||||||
|
else if (jackson2SmilePresent) {
|
||||||
addCodec(this.objectReaders, new DecoderHttpMessageReader<>(this.jackson2SmileDecoder != null ?
|
addCodec(this.objectReaders, new DecoderHttpMessageReader<>(this.jackson2SmileDecoder != null ?
|
||||||
(Jackson2SmileDecoder) this.jackson2SmileDecoder : new Jackson2SmileDecoder()));
|
(Jackson2SmileDecoder) this.jackson2SmileDecoder : new Jackson2SmileDecoder()));
|
||||||
}
|
}
|
||||||
|
@ -711,13 +772,20 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
|
||||||
(KotlinSerializationProtobufEncoder) this.kotlinSerializationProtobufEncoder :
|
(KotlinSerializationProtobufEncoder) this.kotlinSerializationProtobufEncoder :
|
||||||
new KotlinSerializationProtobufEncoder()));
|
new KotlinSerializationProtobufEncoder()));
|
||||||
}
|
}
|
||||||
if (jackson2Present) {
|
if (jacksonPresent) {
|
||||||
|
addCodec(writers, new EncoderHttpMessageWriter<>(getJacksonJsonEncoder()));
|
||||||
|
}
|
||||||
|
else if (jackson2Present) {
|
||||||
addCodec(writers, new EncoderHttpMessageWriter<>(getJackson2JsonEncoder()));
|
addCodec(writers, new EncoderHttpMessageWriter<>(getJackson2JsonEncoder()));
|
||||||
}
|
}
|
||||||
else if (kotlinSerializationJsonPresent) {
|
else if (kotlinSerializationJsonPresent) {
|
||||||
addCodec(writers, new EncoderHttpMessageWriter<>(getKotlinSerializationJsonEncoder()));
|
addCodec(writers, new EncoderHttpMessageWriter<>(getKotlinSerializationJsonEncoder()));
|
||||||
}
|
}
|
||||||
if (jackson2SmilePresent) {
|
if (jacksonSmilePresent) {
|
||||||
|
addCodec(writers, new EncoderHttpMessageWriter<>(this.jacksonSmileEncoder != null ?
|
||||||
|
(JacksonSmileEncoder) this.jacksonSmileEncoder : new JacksonSmileEncoder()));
|
||||||
|
}
|
||||||
|
else if (jackson2SmilePresent) {
|
||||||
addCodec(writers, new EncoderHttpMessageWriter<>(this.jackson2SmileEncoder != null ?
|
addCodec(writers, new EncoderHttpMessageWriter<>(this.jackson2SmileEncoder != null ?
|
||||||
(Jackson2SmileEncoder) this.jackson2SmileEncoder : new Jackson2SmileEncoder()));
|
(Jackson2SmileEncoder) this.jackson2SmileEncoder : new Jackson2SmileEncoder()));
|
||||||
}
|
}
|
||||||
|
@ -764,6 +832,13 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
|
||||||
|
|
||||||
// Accessors for use in subclasses...
|
// Accessors for use in subclasses...
|
||||||
|
|
||||||
|
protected Decoder<?> getJacksonJsonDecoder() {
|
||||||
|
if (this.jacksonJsonDecoder == null) {
|
||||||
|
this.jacksonJsonDecoder = new JacksonJsonDecoder();
|
||||||
|
}
|
||||||
|
return this.jacksonJsonDecoder;
|
||||||
|
}
|
||||||
|
|
||||||
protected Decoder<?> getJackson2JsonDecoder() {
|
protected Decoder<?> getJackson2JsonDecoder() {
|
||||||
if (this.jackson2JsonDecoder == null) {
|
if (this.jackson2JsonDecoder == null) {
|
||||||
this.jackson2JsonDecoder = new Jackson2JsonDecoder();
|
this.jackson2JsonDecoder = new Jackson2JsonDecoder();
|
||||||
|
@ -771,6 +846,13 @@ class BaseDefaultCodecs implements CodecConfigurer.DefaultCodecs, CodecConfigure
|
||||||
return this.jackson2JsonDecoder;
|
return this.jackson2JsonDecoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected Encoder<?> getJacksonJsonEncoder() {
|
||||||
|
if (this.jacksonJsonEncoder == null) {
|
||||||
|
this.jacksonJsonEncoder = new JacksonJsonEncoder();
|
||||||
|
}
|
||||||
|
return this.jacksonJsonEncoder;
|
||||||
|
}
|
||||||
|
|
||||||
protected Encoder<?> getJackson2JsonEncoder() {
|
protected Encoder<?> getJackson2JsonEncoder() {
|
||||||
if (this.jackson2JsonEncoder == null) {
|
if (this.jackson2JsonEncoder == null) {
|
||||||
this.jackson2JsonEncoder = new Jackson2JsonEncoder();
|
this.jackson2JsonEncoder = new Jackson2JsonEncoder();
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2022 the original author or authors.
|
* Copyright 2002-2025 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -53,6 +53,7 @@ class ClientDefaultCodecsImpl extends BaseDefaultCodecs implements ClientCodecCo
|
||||||
protected void extendObjectReaders(List<HttpMessageReader<?>> objectReaders) {
|
protected void extendObjectReaders(List<HttpMessageReader<?>> objectReaders) {
|
||||||
|
|
||||||
Decoder<?> decoder = (this.sseDecoder != null ? this.sseDecoder :
|
Decoder<?> decoder = (this.sseDecoder != null ? this.sseDecoder :
|
||||||
|
jacksonPresent ? getJacksonJsonDecoder() :
|
||||||
jackson2Present ? getJackson2JsonDecoder() :
|
jackson2Present ? getJackson2JsonDecoder() :
|
||||||
kotlinSerializationJsonPresent ? getKotlinSerializationJsonDecoder() :
|
kotlinSerializationJsonPresent ? getKotlinSerializationJsonDecoder() :
|
||||||
null);
|
null);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2022 the original author or authors.
|
* Copyright 2002-2025 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -57,6 +57,7 @@ class ServerDefaultCodecsImpl extends BaseDefaultCodecs implements ServerCodecCo
|
||||||
|
|
||||||
private @Nullable Encoder<?> getSseEncoder() {
|
private @Nullable Encoder<?> getSseEncoder() {
|
||||||
return this.sseEncoder != null ? this.sseEncoder :
|
return this.sseEncoder != null ? this.sseEncoder :
|
||||||
|
jacksonPresent ? getJacksonJsonEncoder() :
|
||||||
jackson2Present ? getJackson2JsonEncoder() :
|
jackson2Present ? getJackson2JsonEncoder() :
|
||||||
kotlinSerializationJsonPresent ? getKotlinSerializationJsonEncoder() :
|
kotlinSerializationJsonPresent ? getKotlinSerializationJsonEncoder() :
|
||||||
null;
|
null;
|
||||||
|
|
|
@ -0,0 +1,376 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2025 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.nio.charset.StandardCharsets;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import io.netty.buffer.ByteBuf;
|
||||||
|
import io.netty.buffer.ByteBufAllocator;
|
||||||
|
import io.netty.buffer.CompositeByteBuf;
|
||||||
|
import io.netty.buffer.UnpooledByteBufAllocator;
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.skyscreamer.jsonassert.JSONAssert;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.test.StepVerifier;
|
||||||
|
import tools.jackson.core.JsonParser;
|
||||||
|
import tools.jackson.core.JsonToken;
|
||||||
|
import tools.jackson.core.TreeNode;
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
import tools.jackson.databind.json.JsonMapper;
|
||||||
|
import tools.jackson.databind.util.TokenBuffer;
|
||||||
|
|
||||||
|
import org.springframework.core.codec.DecodingException;
|
||||||
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
|
import org.springframework.core.io.buffer.DataBufferLimitException;
|
||||||
|
import org.springframework.core.io.buffer.NettyDataBufferFactory;
|
||||||
|
import org.springframework.core.testfixture.io.buffer.AbstractLeakCheckingTests;
|
||||||
|
|
||||||
|
import static java.util.Arrays.asList;
|
||||||
|
import static java.util.Collections.singletonList;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link JacksonTokenizer}.
|
||||||
|
*
|
||||||
|
* @author Sebastien Deleuze
|
||||||
|
*/
|
||||||
|
class JacksonTokenizerTests extends AbstractLeakCheckingTests {
|
||||||
|
|
||||||
|
private ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void createParser() {
|
||||||
|
this.objectMapper = JsonMapper.builder().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void doNotTokenizeArrayElements() {
|
||||||
|
testTokenize(
|
||||||
|
singletonList("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}"),
|
||||||
|
singletonList("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}"), false);
|
||||||
|
|
||||||
|
testTokenize(
|
||||||
|
asList(
|
||||||
|
"{\"foo\": \"foofoo\"",
|
||||||
|
", \"bar\": \"barbar\"}"
|
||||||
|
),
|
||||||
|
singletonList("{\"foo\":\"foofoo\",\"bar\":\"barbar\"}"), false);
|
||||||
|
|
||||||
|
testTokenize(
|
||||||
|
singletonList("[{\"foo\": \"foofoo\", \"bar\": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]"),
|
||||||
|
singletonList("[{\"foo\": \"foofoo\", \"bar\": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]"),
|
||||||
|
false);
|
||||||
|
|
||||||
|
testTokenize(
|
||||||
|
singletonList("[{\"foo\": \"bar\"},{\"foo\": \"baz\"}]"),
|
||||||
|
singletonList("[{\"foo\": \"bar\"},{\"foo\": \"baz\"}]"), false);
|
||||||
|
|
||||||
|
testTokenize(
|
||||||
|
asList(
|
||||||
|
"[{\"foo\": \"foofoo\", \"bar\"",
|
||||||
|
": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]"
|
||||||
|
),
|
||||||
|
singletonList("[{\"foo\": \"foofoo\", \"bar\": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]"),
|
||||||
|
false);
|
||||||
|
|
||||||
|
testTokenize(
|
||||||
|
asList(
|
||||||
|
"[",
|
||||||
|
"{\"id\":1,\"name\":\"Robert\"}", ",",
|
||||||
|
"{\"id\":2,\"name\":\"Raide\"}", ",",
|
||||||
|
"{\"id\":3,\"name\":\"Ford\"}", "]"
|
||||||
|
),
|
||||||
|
singletonList("[{\"id\":1,\"name\":\"Robert\"},{\"id\":2,\"name\":\"Raide\"},{\"id\":3,\"name\":\"Ford\"}]"),
|
||||||
|
false);
|
||||||
|
|
||||||
|
// SPR-16166: top-level JSON values
|
||||||
|
testTokenize(asList("\"foo", "bar\""), singletonList("\"foobar\""), false);
|
||||||
|
testTokenize(asList("12", "34"), singletonList("1234"), false);
|
||||||
|
testTokenize(asList("12.", "34"), singletonList("12.34"), false);
|
||||||
|
|
||||||
|
// note that we do not test for null, true, or false, which are also valid top-level values,
|
||||||
|
// but are unsupported by JSONassert
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void tokenizeArrayElements() {
|
||||||
|
testTokenize(
|
||||||
|
singletonList("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}"),
|
||||||
|
singletonList("{\"foo\": \"foofoo\", \"bar\": \"barbar\"}"), true);
|
||||||
|
|
||||||
|
testTokenize(
|
||||||
|
asList(
|
||||||
|
"{\"foo\": \"foofoo\"",
|
||||||
|
", \"bar\": \"barbar\"}"
|
||||||
|
),
|
||||||
|
singletonList("{\"foo\":\"foofoo\",\"bar\":\"barbar\"}"), true);
|
||||||
|
|
||||||
|
testTokenize(
|
||||||
|
singletonList("[{\"foo\": \"foofoo\", \"bar\": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]"),
|
||||||
|
asList(
|
||||||
|
"{\"foo\": \"foofoo\", \"bar\": \"barbar\"}",
|
||||||
|
"{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}"
|
||||||
|
),
|
||||||
|
true);
|
||||||
|
|
||||||
|
testTokenize(
|
||||||
|
singletonList("[{\"foo\": \"bar\"},{\"foo\": \"baz\"}]"),
|
||||||
|
asList(
|
||||||
|
"{\"foo\": \"bar\"}",
|
||||||
|
"{\"foo\": \"baz\"}"
|
||||||
|
),
|
||||||
|
true);
|
||||||
|
|
||||||
|
// SPR-15803: nested array
|
||||||
|
testTokenize(
|
||||||
|
singletonList("[" +
|
||||||
|
"{\"id\":\"0\",\"start\":[-999999999,1,1],\"end\":[999999999,12,31]}," +
|
||||||
|
"{\"id\":\"1\",\"start\":[-999999999,1,1],\"end\":[999999999,12,31]}," +
|
||||||
|
"{\"id\":\"2\",\"start\":[-999999999,1,1],\"end\":[999999999,12,31]}" +
|
||||||
|
"]"),
|
||||||
|
asList(
|
||||||
|
"{\"id\":\"0\",\"start\":[-999999999,1,1],\"end\":[999999999,12,31]}",
|
||||||
|
"{\"id\":\"1\",\"start\":[-999999999,1,1],\"end\":[999999999,12,31]}",
|
||||||
|
"{\"id\":\"2\",\"start\":[-999999999,1,1],\"end\":[999999999,12,31]}"
|
||||||
|
),
|
||||||
|
true);
|
||||||
|
|
||||||
|
// SPR-15803: nested array, no top-level array
|
||||||
|
testTokenize(
|
||||||
|
singletonList("{\"speakerIds\":[\"tastapod\"],\"language\":\"ENGLISH\"}"),
|
||||||
|
singletonList("{\"speakerIds\":[\"tastapod\"],\"language\":\"ENGLISH\"}"), true);
|
||||||
|
|
||||||
|
testTokenize(
|
||||||
|
asList(
|
||||||
|
"[{\"foo\": \"foofoo\", \"bar\"",
|
||||||
|
": \"barbar\"},{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}]"
|
||||||
|
),
|
||||||
|
asList(
|
||||||
|
"{\"foo\": \"foofoo\", \"bar\": \"barbar\"}",
|
||||||
|
"{\"foo\": \"foofoofoo\", \"bar\": \"barbarbar\"}"), true);
|
||||||
|
|
||||||
|
testTokenize(
|
||||||
|
asList(
|
||||||
|
"[",
|
||||||
|
"{\"id\":1,\"name\":\"Robert\"}",
|
||||||
|
",",
|
||||||
|
"{\"id\":2,\"name\":\"Raide\"}",
|
||||||
|
",",
|
||||||
|
"{\"id\":3,\"name\":\"Ford\"}",
|
||||||
|
"]"
|
||||||
|
),
|
||||||
|
asList(
|
||||||
|
"{\"id\":1,\"name\":\"Robert\"}",
|
||||||
|
"{\"id\":2,\"name\":\"Raide\"}",
|
||||||
|
"{\"id\":3,\"name\":\"Ford\"}"
|
||||||
|
),
|
||||||
|
true);
|
||||||
|
|
||||||
|
// SPR-16166: top-level JSON values
|
||||||
|
testTokenize(asList("\"foo", "bar\""), singletonList("\"foobar\""), true);
|
||||||
|
testTokenize(asList("12", "34"), singletonList("1234"), true);
|
||||||
|
testTokenize(asList("12.", "34"), singletonList("12.34"), true);
|
||||||
|
|
||||||
|
// SPR-16407
|
||||||
|
testTokenize(asList("[1", ",2,", "3]"), asList("1", "2", "3"), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void tokenizeStream() {
|
||||||
|
|
||||||
|
// NDJSON (Newline Delimited JSON), JSON Lines
|
||||||
|
testTokenize(
|
||||||
|
asList(
|
||||||
|
"{\"id\":1,\"name\":\"Robert\"}",
|
||||||
|
"\n",
|
||||||
|
"{\"id\":2,\"name\":\"Raide\"}",
|
||||||
|
"\n",
|
||||||
|
"{\"id\":3,\"name\":\"Ford\"}"
|
||||||
|
),
|
||||||
|
asList(
|
||||||
|
"{\"id\":1,\"name\":\"Robert\"}",
|
||||||
|
"{\"id\":2,\"name\":\"Raide\"}",
|
||||||
|
"{\"id\":3,\"name\":\"Ford\"}"
|
||||||
|
),
|
||||||
|
true);
|
||||||
|
|
||||||
|
// JSON Sequence with newline separator
|
||||||
|
testTokenize(
|
||||||
|
asList(
|
||||||
|
"\n",
|
||||||
|
"{\"id\":1,\"name\":\"Robert\"}",
|
||||||
|
"\n",
|
||||||
|
"{\"id\":2,\"name\":\"Raide\"}",
|
||||||
|
"\n",
|
||||||
|
"{\"id\":3,\"name\":\"Ford\"}"
|
||||||
|
),
|
||||||
|
asList(
|
||||||
|
"{\"id\":1,\"name\":\"Robert\"}",
|
||||||
|
"{\"id\":2,\"name\":\"Raide\"}",
|
||||||
|
"{\"id\":3,\"name\":\"Ford\"}"
|
||||||
|
),
|
||||||
|
true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testTokenize(List<String> input, List<String> output, boolean tokenize) {
|
||||||
|
StepVerifier.FirstStep<String> builder = StepVerifier.create(decode(input, tokenize, -1));
|
||||||
|
output.forEach(expected -> builder.assertNext(actual -> {
|
||||||
|
try {
|
||||||
|
JSONAssert.assertEquals(expected, actual, true);
|
||||||
|
}
|
||||||
|
catch (JSONException ex) {
|
||||||
|
throw new RuntimeException(ex);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
builder.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testLimit() {
|
||||||
|
List<String> source = asList(
|
||||||
|
"[",
|
||||||
|
"{", "\"id\":1,\"name\":\"Dan\"", "},",
|
||||||
|
"{", "\"id\":2,\"name\":\"Ron\"", "},",
|
||||||
|
"{", "\"id\":3,\"name\":\"Bartholomew\"", "}",
|
||||||
|
"]"
|
||||||
|
);
|
||||||
|
|
||||||
|
String expected = String.join("", source);
|
||||||
|
int maxInMemorySize = expected.length();
|
||||||
|
|
||||||
|
StepVerifier.create(decode(source, false, maxInMemorySize))
|
||||||
|
.expectNext(expected)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
StepVerifier.create(decode(source, false, maxInMemorySize - 2))
|
||||||
|
.verifyError(DataBufferLimitException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testLimitTokenized() {
|
||||||
|
|
||||||
|
List<String> source = asList(
|
||||||
|
"[",
|
||||||
|
"{", "\"id\":1, \"name\":\"Dan\"", "},",
|
||||||
|
"{", "\"id\":2, \"name\":\"Ron\"", "},",
|
||||||
|
"{", "\"id\":3, \"name\":\"Bartholomew\"", "}",
|
||||||
|
"]"
|
||||||
|
);
|
||||||
|
|
||||||
|
String expected = "{\"id\":3,\"name\":\"Bartholomew\"}";
|
||||||
|
int maxInMemorySize = expected.length();
|
||||||
|
|
||||||
|
StepVerifier.create(decode(source, true, maxInMemorySize))
|
||||||
|
.expectNext("{\"id\":1,\"name\":\"Dan\"}")
|
||||||
|
.expectNext("{\"id\":2,\"name\":\"Ron\"}")
|
||||||
|
.expectNext(expected)
|
||||||
|
.verifyComplete();
|
||||||
|
|
||||||
|
StepVerifier.create(decode(source, true, maxInMemorySize - 1))
|
||||||
|
.expectNext("{\"id\":1,\"name\":\"Dan\"}")
|
||||||
|
.expectNext("{\"id\":2,\"name\":\"Ron\"}")
|
||||||
|
.verifyError(DataBufferLimitException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void errorInStream() {
|
||||||
|
DataBuffer buffer = stringBuffer("{\"id\":1,\"name\":");
|
||||||
|
Flux<DataBuffer> source = Flux.just(buffer).concatWith(Flux.error(new RuntimeException()));
|
||||||
|
Flux<TokenBuffer> result = JacksonTokenizer.tokenize(source, this.objectMapper, true, false, -1);
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.expectError(RuntimeException.class)
|
||||||
|
.verify();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test // SPR-16521
|
||||||
|
public void jsonEOFExceptionIsWrappedAsDecodingError() {
|
||||||
|
Flux<DataBuffer> source = Flux.just(stringBuffer("{\"status\": \"noClosingQuote}"));
|
||||||
|
Flux<TokenBuffer> tokens = JacksonTokenizer.tokenize(source, this.objectMapper, false, false, -1);
|
||||||
|
|
||||||
|
StepVerifier.create(tokens)
|
||||||
|
.expectError(DecodingException.class)
|
||||||
|
.verify();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void useBigDecimalForFloats() {
|
||||||
|
Flux<DataBuffer> source = Flux.just(stringBuffer("1E+2"));
|
||||||
|
Flux<TokenBuffer> tokens = JacksonTokenizer.tokenize(
|
||||||
|
source, this.objectMapper, false, true, -1);
|
||||||
|
|
||||||
|
StepVerifier.create(tokens)
|
||||||
|
.assertNext(tokenBuffer -> {
|
||||||
|
JsonParser parser = tokenBuffer.asParser();
|
||||||
|
JsonToken token = parser.nextToken();
|
||||||
|
assertThat(token).isEqualTo(JsonToken.VALUE_NUMBER_FLOAT);
|
||||||
|
JsonParser.NumberType numberType = parser.getNumberType();
|
||||||
|
assertThat(numberType).isEqualTo(JsonParser.NumberType.BIG_DECIMAL);
|
||||||
|
})
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// gh-31747
|
||||||
|
@Test
|
||||||
|
void compositeNettyBuffer() {
|
||||||
|
ByteBufAllocator allocator = UnpooledByteBufAllocator.DEFAULT;
|
||||||
|
ByteBuf firstByteBuf = allocator.buffer();
|
||||||
|
firstByteBuf.writeBytes("{\"foo\": \"foofoo\"".getBytes(StandardCharsets.UTF_8));
|
||||||
|
ByteBuf secondBuf = allocator.buffer();
|
||||||
|
secondBuf.writeBytes(", \"bar\": \"barbar\"}".getBytes(StandardCharsets.UTF_8));
|
||||||
|
CompositeByteBuf composite = allocator.compositeBuffer();
|
||||||
|
composite.addComponent(true, firstByteBuf);
|
||||||
|
composite.addComponent(true, secondBuf);
|
||||||
|
|
||||||
|
NettyDataBufferFactory bufferFactory = new NettyDataBufferFactory(allocator);
|
||||||
|
Flux<DataBuffer> source = Flux.just(bufferFactory.wrap(composite));
|
||||||
|
Flux<TokenBuffer> tokens = JacksonTokenizer.tokenize(source, this.objectMapper, false, false, -1);
|
||||||
|
|
||||||
|
Flux<String> strings = tokens.map(this::tokenToString);
|
||||||
|
|
||||||
|
StepVerifier.create(strings)
|
||||||
|
.assertNext(s -> assertThat(s).isEqualTo("{\"foo\":\"foofoo\",\"bar\":\"barbar\"}"))
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private Flux<String> decode(List<String> source, boolean tokenize, int maxInMemorySize) {
|
||||||
|
|
||||||
|
Flux<TokenBuffer> tokens = JacksonTokenizer.tokenize(
|
||||||
|
Flux.fromIterable(source).map(this::stringBuffer), this.objectMapper, tokenize, false, maxInMemorySize);
|
||||||
|
|
||||||
|
return tokens.map(this::tokenToString);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String tokenToString(TokenBuffer tokenBuffer) {
|
||||||
|
TreeNode root = this.objectMapper.readTree(tokenBuffer.asParser());
|
||||||
|
return this.objectMapper.writeValueAsString(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
private DataBuffer stringBuffer(String value) {
|
||||||
|
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
|
||||||
|
DataBuffer buffer = this.bufferFactory.allocateBuffer(bytes.length);
|
||||||
|
buffer.write(bytes);
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -31,6 +31,7 @@ import org.springframework.core.io.buffer.DataBufferLimitException;
|
||||||
import org.springframework.core.testfixture.io.buffer.AbstractLeakCheckingTests;
|
import org.springframework.core.testfixture.io.buffer.AbstractLeakCheckingTests;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.codec.json.Jackson2JsonDecoder;
|
import org.springframework.http.codec.json.Jackson2JsonDecoder;
|
||||||
|
import org.springframework.http.codec.json.JacksonJsonDecoder;
|
||||||
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest;
|
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest;
|
||||||
import org.springframework.web.testfixture.xml.Pojo;
|
import org.springframework.web.testfixture.xml.Pojo;
|
||||||
|
|
||||||
|
@ -44,7 +45,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||||
*/
|
*/
|
||||||
class ServerSentEventHttpMessageReaderTests extends AbstractLeakCheckingTests {
|
class ServerSentEventHttpMessageReaderTests extends AbstractLeakCheckingTests {
|
||||||
|
|
||||||
private final Jackson2JsonDecoder jsonDecoder = new Jackson2JsonDecoder();
|
private final JacksonJsonDecoder jsonDecoder = new JacksonJsonDecoder();
|
||||||
|
|
||||||
private ServerSentEventHttpMessageReader reader = new ServerSentEventHttpMessageReader(this.jsonDecoder);
|
private ServerSentEventHttpMessageReader reader = new ServerSentEventHttpMessageReader(this.jsonDecoder);
|
||||||
|
|
||||||
|
|
|
@ -22,19 +22,19 @@ import java.time.Duration;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import org.reactivestreams.Publisher;
|
import org.reactivestreams.Publisher;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
import reactor.test.StepVerifier;
|
import reactor.test.StepVerifier;
|
||||||
|
import tools.jackson.databind.SerializationFeature;
|
||||||
|
import tools.jackson.databind.json.JsonMapper;
|
||||||
|
|
||||||
import org.springframework.core.ResolvableType;
|
import org.springframework.core.ResolvableType;
|
||||||
import org.springframework.core.io.buffer.DataBufferFactory;
|
import org.springframework.core.io.buffer.DataBufferFactory;
|
||||||
import org.springframework.core.io.buffer.DataBufferUtils;
|
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||||
import org.springframework.core.testfixture.io.buffer.AbstractDataBufferAllocatingTests;
|
import org.springframework.core.testfixture.io.buffer.AbstractDataBufferAllocatingTests;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.codec.json.Jackson2JsonEncoder;
|
import org.springframework.http.codec.json.JacksonJsonEncoder;
|
||||||
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
|
||||||
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpResponse;
|
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpResponse;
|
||||||
import org.springframework.web.testfixture.xml.Pojo;
|
import org.springframework.web.testfixture.xml.Pojo;
|
||||||
|
|
||||||
|
@ -54,7 +54,7 @@ class ServerSentEventHttpMessageWriterTests extends AbstractDataBufferAllocating
|
||||||
private static final Map<String, Object> HINTS = Collections.emptyMap();
|
private static final Map<String, Object> HINTS = Collections.emptyMap();
|
||||||
|
|
||||||
private ServerSentEventHttpMessageWriter messageWriter =
|
private ServerSentEventHttpMessageWriter messageWriter =
|
||||||
new ServerSentEventHttpMessageWriter(new Jackson2JsonEncoder());
|
new ServerSentEventHttpMessageWriter(new JacksonJsonEncoder());
|
||||||
|
|
||||||
|
|
||||||
@ParameterizedDataBufferAllocatingTest
|
@ParameterizedDataBufferAllocatingTest
|
||||||
|
@ -151,10 +151,10 @@ class ServerSentEventHttpMessageWriterTests extends AbstractDataBufferAllocating
|
||||||
|
|
||||||
StepVerifier.create(outputMessage.getBody())
|
StepVerifier.create(outputMessage.getBody())
|
||||||
.consumeNextWith(stringConsumer("data:"))
|
.consumeNextWith(stringConsumer("data:"))
|
||||||
.consumeNextWith(stringConsumer("{\"foo\":\"foofoo\",\"bar\":\"barbar\"}"))
|
.consumeNextWith(stringConsumer("{\"bar\":\"barbar\",\"foo\":\"foofoo\"}"))
|
||||||
.consumeNextWith(stringConsumer("\n\n"))
|
.consumeNextWith(stringConsumer("\n\n"))
|
||||||
.consumeNextWith(stringConsumer("data:"))
|
.consumeNextWith(stringConsumer("data:"))
|
||||||
.consumeNextWith(stringConsumer("{\"foo\":\"foofoofoo\",\"bar\":\"barbarbar\"}"))
|
.consumeNextWith(stringConsumer("{\"bar\":\"barbarbar\",\"foo\":\"foofoofoo\"}"))
|
||||||
.consumeNextWith(stringConsumer("\n\n"))
|
.consumeNextWith(stringConsumer("\n\n"))
|
||||||
.expectComplete()
|
.expectComplete()
|
||||||
.verify();
|
.verify();
|
||||||
|
@ -164,8 +164,8 @@ class ServerSentEventHttpMessageWriterTests extends AbstractDataBufferAllocating
|
||||||
void writePojoWithPrettyPrint(DataBufferFactory bufferFactory) {
|
void writePojoWithPrettyPrint(DataBufferFactory bufferFactory) {
|
||||||
super.bufferFactory = bufferFactory;
|
super.bufferFactory = bufferFactory;
|
||||||
|
|
||||||
ObjectMapper mapper = Jackson2ObjectMapperBuilder.json().indentOutput(true).build();
|
JsonMapper mapper = JsonMapper.builder().enable(SerializationFeature.INDENT_OUTPUT).build();
|
||||||
this.messageWriter = new ServerSentEventHttpMessageWriter(new Jackson2JsonEncoder(mapper));
|
this.messageWriter = new ServerSentEventHttpMessageWriter(new JacksonJsonEncoder(mapper));
|
||||||
|
|
||||||
MockServerHttpResponse outputMessage = new MockServerHttpResponse(super.bufferFactory);
|
MockServerHttpResponse outputMessage = new MockServerHttpResponse(super.bufferFactory);
|
||||||
Flux<Pojo> source = Flux.just(new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar"));
|
Flux<Pojo> source = Flux.just(new Pojo("foofoo", "barbar"), new Pojo("foofoofoo", "barbarbar"));
|
||||||
|
@ -175,15 +175,15 @@ class ServerSentEventHttpMessageWriterTests extends AbstractDataBufferAllocating
|
||||||
.consumeNextWith(stringConsumer("data:"))
|
.consumeNextWith(stringConsumer("data:"))
|
||||||
.consumeNextWith(stringConsumer("""
|
.consumeNextWith(stringConsumer("""
|
||||||
{
|
{
|
||||||
data: "foo" : "foofoo",
|
data: "bar" : "barbar",
|
||||||
data: "bar" : "barbar"
|
data: "foo" : "foofoo"
|
||||||
data:}"""))
|
data:}"""))
|
||||||
.consumeNextWith(stringConsumer("\n\n"))
|
.consumeNextWith(stringConsumer("\n\n"))
|
||||||
.consumeNextWith(stringConsumer("data:"))
|
.consumeNextWith(stringConsumer("data:"))
|
||||||
.consumeNextWith(stringConsumer("""
|
.consumeNextWith(stringConsumer("""
|
||||||
{
|
{
|
||||||
data: "foo" : "foofoofoo",
|
data: "bar" : "barbarbar",
|
||||||
data: "bar" : "barbarbar"
|
data: "foo" : "foofoofoo"
|
||||||
data:}"""))
|
data:}"""))
|
||||||
.consumeNextWith(stringConsumer("\n\n"))
|
.consumeNextWith(stringConsumer("\n\n"))
|
||||||
.expectComplete()
|
.expectComplete()
|
||||||
|
@ -203,7 +203,7 @@ class ServerSentEventHttpMessageWriterTests extends AbstractDataBufferAllocating
|
||||||
assertThat(outputMessage.getHeaders().getContentType()).isEqualTo(mediaType);
|
assertThat(outputMessage.getHeaders().getContentType()).isEqualTo(mediaType);
|
||||||
StepVerifier.create(outputMessage.getBody())
|
StepVerifier.create(outputMessage.getBody())
|
||||||
.consumeNextWith(stringConsumer("data:", charset))
|
.consumeNextWith(stringConsumer("data:", charset))
|
||||||
.consumeNextWith(stringConsumer("{\"foo\":\"foo\uD834\uDD1E\",\"bar\":\"bar\uD834\uDD1E\"}", charset))
|
.consumeNextWith(stringConsumer("{\"bar\":\"bar\uD834\uDD1E\",\"foo\":\"foo\uD834\uDD1E\"}", charset))
|
||||||
.consumeNextWith(stringConsumer("\n\n", charset))
|
.consumeNextWith(stringConsumer("\n\n", charset))
|
||||||
.expectComplete()
|
.expectComplete()
|
||||||
.verify();
|
.verify();
|
||||||
|
|
|
@ -0,0 +1,103 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2025 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 java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import tools.jackson.core.JacksonException;
|
||||||
|
import tools.jackson.dataformat.cbor.CBORMapper;
|
||||||
|
|
||||||
|
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 org.springframework.web.testfixture.xml.Pojo;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
|
import static org.springframework.http.MediaType.APPLICATION_JSON;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link JacksonCborDecoder}.
|
||||||
|
*
|
||||||
|
* @author Sebastien Deleuze
|
||||||
|
*/
|
||||||
|
class JacksonCborDecoderTests extends AbstractDecoderTests<JacksonCborDecoder> {
|
||||||
|
|
||||||
|
private Pojo pojo1 = new Pojo("f1", "b1");
|
||||||
|
|
||||||
|
private Pojo pojo2 = new Pojo("f2", "b2");
|
||||||
|
|
||||||
|
private CBORMapper mapper = CBORMapper.builder().build();
|
||||||
|
|
||||||
|
public JacksonCborDecoderTests() {
|
||||||
|
super(new JacksonCborDecoder());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Test
|
||||||
|
protected void canDecode() {
|
||||||
|
assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), MediaType.APPLICATION_CBOR)).isTrue();
|
||||||
|
assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), null)).isTrue();
|
||||||
|
|
||||||
|
assertThat(decoder.canDecode(ResolvableType.forClass(String.class), null)).isFalse();
|
||||||
|
assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), APPLICATION_JSON)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Test
|
||||||
|
protected void decode() {
|
||||||
|
Flux<DataBuffer> input = Flux.just(this.pojo1, this.pojo2)
|
||||||
|
.map(this::writeObject)
|
||||||
|
.flatMap(this::dataBuffer);
|
||||||
|
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() ->
|
||||||
|
testDecodeAll(input, Pojo.class, step -> step
|
||||||
|
.expectNext(pojo1)
|
||||||
|
.expectNext(pojo2)
|
||||||
|
.verifyComplete()));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] writeObject(Object o) {
|
||||||
|
try {
|
||||||
|
return this.mapper.writer().writeValueAsBytes(o);
|
||||||
|
}
|
||||||
|
catch (JacksonException e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Test
|
||||||
|
protected void decodeToMono() {
|
||||||
|
List<Pojo> expected = Arrays.asList(pojo1, pojo2);
|
||||||
|
|
||||||
|
Flux<DataBuffer> input = Flux.just(expected)
|
||||||
|
.map(this::writeObject)
|
||||||
|
.flatMap(this::dataBuffer);
|
||||||
|
|
||||||
|
ResolvableType elementType = ResolvableType.forClassWithGenerics(List.class, Pojo.class);
|
||||||
|
testDecodeToMono(input, elementType, step -> step
|
||||||
|
.expectNext(expected)
|
||||||
|
.expectComplete()
|
||||||
|
.verify(), null, null);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2025 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 java.util.function.Consumer;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import tools.jackson.dataformat.cbor.CBORMapper;
|
||||||
|
|
||||||
|
import org.springframework.core.ResolvableType;
|
||||||
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
|
import org.springframework.core.testfixture.io.buffer.AbstractLeakCheckingTests;
|
||||||
|
import org.springframework.core.testfixture.io.buffer.DataBufferTestUtils;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.web.testfixture.xml.Pojo;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
|
import static org.springframework.core.io.buffer.DataBufferUtils.release;
|
||||||
|
import static org.springframework.http.MediaType.APPLICATION_XML;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link JacksonCborEncoder}.
|
||||||
|
*
|
||||||
|
* @author Sebastien Deleuze
|
||||||
|
*/
|
||||||
|
class JacksonCborEncoderTests extends AbstractLeakCheckingTests {
|
||||||
|
|
||||||
|
private final CBORMapper mapper = CBORMapper.builder().build();
|
||||||
|
|
||||||
|
private final JacksonCborEncoder encoder = new JacksonCborEncoder();
|
||||||
|
|
||||||
|
private Consumer<DataBuffer> pojoConsumer(Pojo expected) {
|
||||||
|
return dataBuffer -> {
|
||||||
|
Pojo actual = this.mapper.reader().forType(Pojo.class)
|
||||||
|
.readValue(DataBufferTestUtils.dumpBytes(dataBuffer));
|
||||||
|
assertThat(actual).isEqualTo(expected);
|
||||||
|
release(dataBuffer);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void canEncode() {
|
||||||
|
ResolvableType pojoType = ResolvableType.forClass(Pojo.class);
|
||||||
|
assertThat(this.encoder.canEncode(pojoType, MediaType.APPLICATION_CBOR)).isTrue();
|
||||||
|
assertThat(this.encoder.canEncode(pojoType, null)).isTrue();
|
||||||
|
|
||||||
|
// SPR-15464
|
||||||
|
assertThat(this.encoder.canEncode(ResolvableType.NONE, null)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void canNotEncode() {
|
||||||
|
assertThat(this.encoder.canEncode(ResolvableType.forClass(String.class), null)).isFalse();
|
||||||
|
assertThat(this.encoder.canEncode(ResolvableType.forClass(Pojo.class), APPLICATION_XML)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void encode() {
|
||||||
|
Pojo value = new Pojo("foo", "bar");
|
||||||
|
DataBuffer result = encoder.encodeValue(value, this.bufferFactory, ResolvableType.forClass(Pojo.class),
|
||||||
|
MediaType.APPLICATION_CBOR, null);
|
||||||
|
pojoConsumer(value).accept(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void encodeStream() {
|
||||||
|
Pojo pojo1 = new Pojo("foo", "bar");
|
||||||
|
Pojo pojo2 = new Pojo("foofoo", "barbar");
|
||||||
|
Pojo pojo3 = new Pojo("foofoofoo", "barbarbar");
|
||||||
|
Flux<Pojo> input = Flux.just(pojo1, pojo2, pojo3);
|
||||||
|
ResolvableType type = ResolvableType.forClass(Pojo.class);
|
||||||
|
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() ->
|
||||||
|
encoder.encode(input, this.bufferFactory, type, MediaType.APPLICATION_CBOR, null));
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,123 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2025 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.json;
|
||||||
|
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import tools.jackson.databind.ObjectReader;
|
||||||
|
import tools.jackson.databind.cfg.EnumFeature;
|
||||||
|
|
||||||
|
import org.springframework.core.ResolvableType;
|
||||||
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
|
import org.springframework.core.testfixture.codec.AbstractDecoderTests;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for a customized {@link JacksonJsonDecoder}.
|
||||||
|
*
|
||||||
|
* @author Sebastien Deleuze
|
||||||
|
*/
|
||||||
|
class CustomizedJacksonJsonDecoderTests extends AbstractDecoderTests<JacksonJsonDecoder> {
|
||||||
|
|
||||||
|
CustomizedJacksonJsonDecoderTests() {
|
||||||
|
super(new JacksonJsonDecoderWithCustomization());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void canDecode() throws Exception {
|
||||||
|
// Not Testing, covered under JacksonJsonDecoderTests
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Override
|
||||||
|
public void decode() throws Exception {
|
||||||
|
Flux<DataBuffer> input = Flux.concat(stringBuffer("{\"property\":\"Value1\"}"));
|
||||||
|
|
||||||
|
testDecodeAll(input, MyCustomizedDecoderBean.class, step -> step
|
||||||
|
.expectNextMatches(obj -> obj.getProperty() == MyCustomDecoderEnum.VAL1)
|
||||||
|
.verifyComplete());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Override
|
||||||
|
public void decodeToMono() throws Exception {
|
||||||
|
Mono<DataBuffer> input = stringBuffer("{\"property\":\"Value2\"}");
|
||||||
|
|
||||||
|
ResolvableType elementType = ResolvableType.forClass(MyCustomizedDecoderBean.class);
|
||||||
|
|
||||||
|
testDecodeToMono(input, elementType, step -> step
|
||||||
|
.expectNextMatches(obj -> ((MyCustomizedDecoderBean)obj).getProperty() == MyCustomDecoderEnum.VAL2)
|
||||||
|
.expectComplete()
|
||||||
|
.verify(), null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<DataBuffer> stringBuffer(String value) {
|
||||||
|
return stringBuffer(value, StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<DataBuffer> stringBuffer(String value, Charset charset) {
|
||||||
|
return Mono.defer(() -> {
|
||||||
|
byte[] bytes = value.getBytes(charset);
|
||||||
|
DataBuffer buffer = this.bufferFactory.allocateBuffer(bytes.length);
|
||||||
|
buffer.write(bytes);
|
||||||
|
return Mono.just(buffer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static class MyCustomizedDecoderBean {
|
||||||
|
|
||||||
|
private MyCustomDecoderEnum property;
|
||||||
|
|
||||||
|
public MyCustomDecoderEnum getProperty() {
|
||||||
|
return property;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public void setProperty(MyCustomDecoderEnum property) {
|
||||||
|
this.property = property;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private enum MyCustomDecoderEnum {
|
||||||
|
VAL1,
|
||||||
|
VAL2;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return this == VAL1 ? "Value1" : "Value2";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static class JacksonJsonDecoderWithCustomization extends JacksonJsonDecoder {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ObjectReader customizeReader(
|
||||||
|
ObjectReader reader, ResolvableType elementType, Map<String, Object> hints) {
|
||||||
|
|
||||||
|
return reader.with(EnumFeature.READ_ENUMS_USING_TO_STRING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,121 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2025 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.json;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import tools.jackson.databind.ObjectWriter;
|
||||||
|
import tools.jackson.databind.cfg.EnumFeature;
|
||||||
|
|
||||||
|
import org.springframework.core.ResolvableType;
|
||||||
|
import org.springframework.core.io.buffer.DataBufferUtils;
|
||||||
|
import org.springframework.core.testfixture.codec.AbstractEncoderTests;
|
||||||
|
import org.springframework.util.MimeType;
|
||||||
|
|
||||||
|
import static org.springframework.http.MediaType.APPLICATION_NDJSON;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for a customized {@link JacksonJsonEncoder}.
|
||||||
|
*
|
||||||
|
* @author Sebastien Deleuze
|
||||||
|
*/
|
||||||
|
class CustomizedJacksonJsonEncoderTests extends AbstractEncoderTests<JacksonJsonEncoder> {
|
||||||
|
|
||||||
|
CustomizedJacksonJsonEncoderTests() {
|
||||||
|
super(new JacksonJsonEncoderWithCustomization());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void canEncode() throws Exception {
|
||||||
|
// Not Testing, covered under JacksonJsonEncoderTests
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Override
|
||||||
|
public void encode() throws Exception {
|
||||||
|
Flux<MyCustomizedEncoderBean> input = Flux.just(
|
||||||
|
new MyCustomizedEncoderBean(MyCustomEncoderEnum.VAL1),
|
||||||
|
new MyCustomizedEncoderBean(MyCustomEncoderEnum.VAL2)
|
||||||
|
);
|
||||||
|
|
||||||
|
testEncodeAll(input, ResolvableType.forClass(MyCustomizedEncoderBean.class), APPLICATION_NDJSON, null, step -> step
|
||||||
|
.consumeNextWith(expectString("{\"property\":\"Value1\"}\n"))
|
||||||
|
.consumeNextWith(expectString("{\"property\":\"Value2\"}\n"))
|
||||||
|
.verifyComplete()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void encodeNonStream() {
|
||||||
|
Flux<MyCustomizedEncoderBean> input = Flux.just(
|
||||||
|
new MyCustomizedEncoderBean(MyCustomEncoderEnum.VAL1),
|
||||||
|
new MyCustomizedEncoderBean(MyCustomEncoderEnum.VAL2)
|
||||||
|
);
|
||||||
|
|
||||||
|
testEncode(input, MyCustomizedEncoderBean.class, step -> step
|
||||||
|
.consumeNextWith(expectString("[{\"property\":\"Value1\"}").andThen(DataBufferUtils::release))
|
||||||
|
.consumeNextWith(expectString(",{\"property\":\"Value2\"}").andThen(DataBufferUtils::release))
|
||||||
|
.consumeNextWith(expectString("]").andThen(DataBufferUtils::release))
|
||||||
|
.verifyComplete());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static class MyCustomizedEncoderBean {
|
||||||
|
|
||||||
|
private MyCustomEncoderEnum property;
|
||||||
|
|
||||||
|
public MyCustomizedEncoderBean(MyCustomEncoderEnum property) {
|
||||||
|
this.property = property;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public MyCustomEncoderEnum getProperty() {
|
||||||
|
return property;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
public void setProperty(MyCustomEncoderEnum property) {
|
||||||
|
this.property = property;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private enum MyCustomEncoderEnum {
|
||||||
|
VAL1,
|
||||||
|
VAL2;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return this == VAL1 ? "Value1" : "Value2";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static class JacksonJsonEncoderWithCustomization extends JacksonJsonEncoder {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected ObjectWriter customizeWriter(
|
||||||
|
ObjectWriter writer, MimeType mimeType, ResolvableType elementType, Map<String, Object> hints) {
|
||||||
|
|
||||||
|
return writer.with(EnumFeature.WRITE_ENUMS_USING_TO_STRING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,414 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2025 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.json;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.test.StepVerifier;
|
||||||
|
import tools.jackson.core.JsonParser;
|
||||||
|
import tools.jackson.databind.DeserializationContext;
|
||||||
|
import tools.jackson.databind.JsonNode;
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
import tools.jackson.databind.annotation.JsonDeserialize;
|
||||||
|
import tools.jackson.databind.deser.std.StdDeserializer;
|
||||||
|
import tools.jackson.databind.json.JsonMapper;
|
||||||
|
|
||||||
|
import org.springframework.core.ParameterizedTypeReference;
|
||||||
|
import org.springframework.core.ResolvableType;
|
||||||
|
import org.springframework.core.codec.CodecException;
|
||||||
|
import org.springframework.core.codec.DecodingException;
|
||||||
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
|
import org.springframework.core.testfixture.codec.AbstractDecoderTests;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.codec.json.JacksonViewBean.MyJacksonView1;
|
||||||
|
import org.springframework.http.codec.json.JacksonViewBean.MyJacksonView3;
|
||||||
|
import org.springframework.util.MimeType;
|
||||||
|
import org.springframework.util.MimeTypeUtils;
|
||||||
|
import org.springframework.web.testfixture.xml.Pojo;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
|
import static org.springframework.http.MediaType.APPLICATION_JSON;
|
||||||
|
import static org.springframework.http.MediaType.APPLICATION_NDJSON;
|
||||||
|
import static org.springframework.http.MediaType.APPLICATION_XML;
|
||||||
|
import static org.springframework.http.codec.JacksonCodecSupport.JSON_VIEW_HINT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link JacksonJsonDecoder}.
|
||||||
|
*
|
||||||
|
* @author Sebastien Deleuze
|
||||||
|
*/
|
||||||
|
class JacksonJsonDecoderTests extends AbstractDecoderTests<JacksonJsonDecoder> {
|
||||||
|
|
||||||
|
private final Pojo pojo1 = new Pojo("f1", "b1");
|
||||||
|
|
||||||
|
private final Pojo pojo2 = new Pojo("f2", "b2");
|
||||||
|
|
||||||
|
|
||||||
|
public JacksonJsonDecoderTests() {
|
||||||
|
super(new JacksonJsonDecoder(JsonMapper.builder().build()));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Test
|
||||||
|
public void canDecode() {
|
||||||
|
assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), APPLICATION_JSON)).isTrue();
|
||||||
|
assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), APPLICATION_NDJSON)).isTrue();
|
||||||
|
assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), null)).isTrue();
|
||||||
|
|
||||||
|
assertThat(decoder.canDecode(ResolvableType.forClass(String.class), null)).isFalse();
|
||||||
|
assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), APPLICATION_XML)).isFalse();
|
||||||
|
assertThat(this.decoder.canDecode(ResolvableType.forClass(Pojo.class),
|
||||||
|
new MediaType("application", "json", StandardCharsets.UTF_8))).isTrue();
|
||||||
|
assertThat(this.decoder.canDecode(ResolvableType.forClass(Pojo.class),
|
||||||
|
new MediaType("application", "json", StandardCharsets.US_ASCII))).isTrue();
|
||||||
|
assertThat(this.decoder.canDecode(ResolvableType.forClass(Pojo.class),
|
||||||
|
new MediaType("application", "json", StandardCharsets.ISO_8859_1))).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void canDecodeWithObjectMapperRegistrationForType() {
|
||||||
|
MediaType halJsonMediaType = MediaType.parseMediaType("application/hal+json");
|
||||||
|
MediaType halFormsJsonMediaType = MediaType.parseMediaType("application/prs.hal-forms+json");
|
||||||
|
|
||||||
|
assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), halJsonMediaType)).isTrue();
|
||||||
|
assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), MediaType.APPLICATION_JSON)).isTrue();
|
||||||
|
assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), halFormsJsonMediaType)).isTrue();
|
||||||
|
assertThat(decoder.canDecode(ResolvableType.forClass(Map.class), MediaType.APPLICATION_JSON)).isTrue();
|
||||||
|
|
||||||
|
decoder.registerObjectMappersForType(Pojo.class, map -> {
|
||||||
|
map.put(halJsonMediaType, new ObjectMapper());
|
||||||
|
map.put(MediaType.APPLICATION_JSON, new ObjectMapper());
|
||||||
|
});
|
||||||
|
|
||||||
|
assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), halJsonMediaType)).isTrue();
|
||||||
|
assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), MediaType.APPLICATION_JSON)).isTrue();
|
||||||
|
assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), halFormsJsonMediaType)).isFalse();
|
||||||
|
assertThat(decoder.canDecode(ResolvableType.forClass(Map.class), MediaType.APPLICATION_JSON)).isTrue();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test // SPR-15866
|
||||||
|
void canDecodeWithProvidedMimeType() {
|
||||||
|
MimeType textJavascript = new MimeType("text", "javascript", StandardCharsets.UTF_8);
|
||||||
|
JacksonJsonDecoder decoder = new JacksonJsonDecoder(new ObjectMapper(), textJavascript);
|
||||||
|
|
||||||
|
assertThat(decoder.getDecodableMimeTypes()).isEqualTo(Collections.singletonList(textJavascript));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
void decodableMimeTypesIsImmutable() {
|
||||||
|
MimeType textJavascript = new MimeType("text", "javascript", StandardCharsets.UTF_8);
|
||||||
|
JacksonJsonDecoder decoder = new JacksonJsonDecoder(new ObjectMapper(), textJavascript);
|
||||||
|
|
||||||
|
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() ->
|
||||||
|
decoder.getDecodableMimeTypes().add(new MimeType("text", "ecmascript")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void decodableMimeTypesWithObjectMapperRegistration() {
|
||||||
|
MimeType mimeType1 = MediaType.parseMediaType("application/hal+json");
|
||||||
|
MimeType mimeType2 = new MimeType("text", "javascript", StandardCharsets.UTF_8);
|
||||||
|
|
||||||
|
JacksonJsonDecoder decoder = new JacksonJsonDecoder(new ObjectMapper(), mimeType2);
|
||||||
|
decoder.registerObjectMappersForType(Pojo.class, map -> map.put(mimeType1, new ObjectMapper()));
|
||||||
|
|
||||||
|
assertThat(decoder.getDecodableMimeTypes(ResolvableType.forClass(Pojo.class)))
|
||||||
|
.containsExactly(mimeType1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Test
|
||||||
|
protected void decode() {
|
||||||
|
Flux<DataBuffer> input = Flux.concat(
|
||||||
|
stringBuffer("[{\"bar\":\"b1\",\"foo\":\"f1\"},"),
|
||||||
|
stringBuffer("{\"bar\":\"b2\",\"foo\":\"f2\"}]"));
|
||||||
|
|
||||||
|
testDecodeAll(input, Pojo.class, step -> step
|
||||||
|
.expectNext(pojo1)
|
||||||
|
.expectNext(pojo2)
|
||||||
|
.verifyComplete());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Test
|
||||||
|
protected void decodeToMono() {
|
||||||
|
Flux<DataBuffer> input = Flux.concat(
|
||||||
|
stringBuffer("[{\"bar\":\"b1\",\"foo\":\"f1\"},"),
|
||||||
|
stringBuffer("{\"bar\":\"b2\",\"foo\":\"f2\"}]"));
|
||||||
|
|
||||||
|
ResolvableType elementType = ResolvableType.forClassWithGenerics(List.class, Pojo.class);
|
||||||
|
|
||||||
|
testDecodeToMonoAll(input, elementType, step -> step
|
||||||
|
.expectNext(Arrays.asList(new Pojo("f1", "b1"), new Pojo("f2", "b2")))
|
||||||
|
.expectComplete()
|
||||||
|
.verify(), null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void decodeToFluxWithListElements() {
|
||||||
|
Flux<DataBuffer> input = Flux.concat(
|
||||||
|
stringBuffer("[{\"bar\":\"b1\",\"foo\":\"f1\"},{\"bar\":\"b2\",\"foo\":\"f2\"}]"),
|
||||||
|
stringBuffer("[{\"bar\":\"b3\",\"foo\":\"f3\"},{\"bar\":\"b4\",\"foo\":\"f4\"}]"));
|
||||||
|
|
||||||
|
ResolvableType elementType = ResolvableType.forClassWithGenerics(List.class, Pojo.class);
|
||||||
|
|
||||||
|
testDecodeAll(input, elementType,
|
||||||
|
step -> step
|
||||||
|
.expectNext(List.of(pojo1, pojo2))
|
||||||
|
.expectNext(List.of(new Pojo("f3", "b3"), new Pojo("f4", "b4")))
|
||||||
|
.verifyComplete(),
|
||||||
|
MimeTypeUtils.APPLICATION_JSON,
|
||||||
|
Collections.emptyMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void decodeEmptyArrayToFlux() {
|
||||||
|
Flux<DataBuffer> input = Flux.from(stringBuffer("[]"));
|
||||||
|
|
||||||
|
testDecode(input, Pojo.class, StepVerifier.LastStep::verifyComplete);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fieldLevelJsonView() {
|
||||||
|
Flux<DataBuffer> input = Flux.from(stringBuffer(
|
||||||
|
"{\"withView1\" : \"with\", \"withView2\" : \"with\", \"withoutView\" : \"without\"}"));
|
||||||
|
|
||||||
|
ResolvableType elementType = ResolvableType.forClass(JacksonViewBean.class);
|
||||||
|
Map<String, Object> hints = Collections.singletonMap(JSON_VIEW_HINT, MyJacksonView1.class);
|
||||||
|
|
||||||
|
testDecode(input, elementType, step -> step
|
||||||
|
.consumeNextWith(value -> {
|
||||||
|
JacksonViewBean bean = (JacksonViewBean) value;
|
||||||
|
assertThat(bean.getWithView1()).isEqualTo("with");
|
||||||
|
assertThat(bean.getWithView2()).isNull();
|
||||||
|
assertThat(bean.getWithoutView()).isNull();
|
||||||
|
}), null, hints);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void classLevelJsonView() {
|
||||||
|
Flux<DataBuffer> input = Flux.from(stringBuffer(
|
||||||
|
"{\"withoutView\" : \"without\"}"));
|
||||||
|
|
||||||
|
ResolvableType elementType = ResolvableType.forClass(JacksonViewBean.class);
|
||||||
|
Map<String, Object> hints = Collections.singletonMap(JSON_VIEW_HINT, MyJacksonView3.class);
|
||||||
|
|
||||||
|
testDecode(input, elementType, step -> step
|
||||||
|
.consumeNextWith(value -> {
|
||||||
|
JacksonViewBean bean = (JacksonViewBean) value;
|
||||||
|
assertThat(bean.getWithoutView()).isEqualTo("without");
|
||||||
|
assertThat(bean.getWithView1()).isNull();
|
||||||
|
assertThat(bean.getWithView2()).isNull();
|
||||||
|
})
|
||||||
|
.verifyComplete(), null, hints);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void invalidData() {
|
||||||
|
Flux<DataBuffer> input = Flux.from(stringBuffer("{\"foofoo\": \"foofoo\", \"barbar\": \"barbar\""));
|
||||||
|
testDecode(input, Pojo.class, step -> step.verifyError(DecodingException.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test // gh-22042
|
||||||
|
void decodeWithNullLiteral() {
|
||||||
|
Flux<Object> result = this.decoder.decode(Flux.concat(stringBuffer("null")),
|
||||||
|
ResolvableType.forType(Pojo.class), MediaType.APPLICATION_JSON, Collections.emptyMap());
|
||||||
|
|
||||||
|
StepVerifier.create(result).expectComplete().verify();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test // gh-27511
|
||||||
|
void noDefaultConstructor() {
|
||||||
|
Flux<DataBuffer> input = Flux.from(stringBuffer("{\"property1\":\"foo\",\"property2\":\"bar\"}"));
|
||||||
|
|
||||||
|
testDecode(input, BeanWithNoDefaultConstructor.class, step -> step
|
||||||
|
.consumeNextWith(o -> {
|
||||||
|
assertThat(o.getProperty1()).isEqualTo("foo");
|
||||||
|
assertThat(o.getProperty2()).isEqualTo("bar");
|
||||||
|
})
|
||||||
|
.verifyComplete()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void codecException() {
|
||||||
|
Flux<DataBuffer> input = Flux.from(stringBuffer("["));
|
||||||
|
ResolvableType elementType = ResolvableType.forClass(BeanWithNoDefaultConstructor.class);
|
||||||
|
Flux<Object> flux = new Jackson2JsonDecoder().decode(input, elementType, null, Collections.emptyMap());
|
||||||
|
StepVerifier.create(flux).verifyError(CodecException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test // SPR-15975
|
||||||
|
void customDeserializer() {
|
||||||
|
Mono<DataBuffer> input = stringBuffer("{\"test\": 1}");
|
||||||
|
|
||||||
|
testDecode(input, TestObject.class, step -> step
|
||||||
|
.consumeNextWith(o -> assertThat(o.getTest()).isEqualTo(1))
|
||||||
|
.verifyComplete()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void bigDecimalFlux() {
|
||||||
|
Flux<DataBuffer> input = stringBuffer("[ 1E+2 ]").flux();
|
||||||
|
|
||||||
|
testDecode(input, BigDecimal.class, step -> step
|
||||||
|
.expectNext(new BigDecimal("1E+2"))
|
||||||
|
.verifyComplete()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
void decodeNonUtf8Encoding() {
|
||||||
|
Mono<DataBuffer> input = stringBuffer("{\"foo\":\"bar\"}", StandardCharsets.UTF_16);
|
||||||
|
ResolvableType type = ResolvableType.forType(new ParameterizedTypeReference<Map<String, String>>() {});
|
||||||
|
|
||||||
|
testDecode(input, type, step -> step
|
||||||
|
.assertNext(value -> assertThat((Map<String, String>) value).containsEntry("foo", "bar"))
|
||||||
|
.verifyComplete(),
|
||||||
|
MediaType.parseMediaType("application/json; charset=utf-16"),
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
void decodeNonUnicode() {
|
||||||
|
Flux<DataBuffer> input = Flux.concat(stringBuffer("{\"føø\":\"bår\"}", StandardCharsets.ISO_8859_1));
|
||||||
|
ResolvableType type = ResolvableType.forType(new ParameterizedTypeReference<Map<String, String>>() {});
|
||||||
|
|
||||||
|
testDecode(input, type, step -> step
|
||||||
|
.assertNext(o -> assertThat((Map<String, String>) o).containsEntry("føø", "bår"))
|
||||||
|
.verifyComplete(),
|
||||||
|
MediaType.parseMediaType("application/json; charset=iso-8859-1"),
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
void decodeMonoNonUtf8Encoding() {
|
||||||
|
Mono<DataBuffer> input = stringBuffer("{\"foo\":\"bar\"}", StandardCharsets.UTF_16);
|
||||||
|
ResolvableType type = ResolvableType.forType(new ParameterizedTypeReference<Map<String, String>>() {});
|
||||||
|
|
||||||
|
testDecodeToMono(input, type, step -> step
|
||||||
|
.assertNext(value -> assertThat((Map<String, String>) value).containsEntry("foo", "bar"))
|
||||||
|
.verifyComplete(),
|
||||||
|
MediaType.parseMediaType("application/json; charset=utf-16"),
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
void decodeAscii() {
|
||||||
|
Flux<DataBuffer> input = Flux.concat(stringBuffer("{\"foo\":\"bar\"}", StandardCharsets.US_ASCII));
|
||||||
|
ResolvableType type = ResolvableType.forType(new ParameterizedTypeReference<Map<String, String>>() {});
|
||||||
|
|
||||||
|
testDecode(input, type, step -> step
|
||||||
|
.assertNext(value -> assertThat((Map<String, String>) value).containsEntry("foo", "bar"))
|
||||||
|
.verifyComplete(),
|
||||||
|
MediaType.parseMediaType("application/json; charset=us-ascii"),
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void cancelWhileDecoding() {
|
||||||
|
Flux<DataBuffer> input = Flux.just(
|
||||||
|
stringBuffer("[{\"bar\":\"b1\",\"foo\":\"f1\"},").block(),
|
||||||
|
stringBuffer("{\"bar\":\"b2\",\"foo\":\"f2\"}]").block());
|
||||||
|
|
||||||
|
testDecodeCancel(input, ResolvableType.forClass(Pojo.class), null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private Mono<DataBuffer> stringBuffer(String value) {
|
||||||
|
return stringBuffer(value, StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Mono<DataBuffer> stringBuffer(String value, Charset charset) {
|
||||||
|
return Mono.defer(() -> {
|
||||||
|
byte[] bytes = value.getBytes(charset);
|
||||||
|
DataBuffer buffer = this.bufferFactory.allocateBuffer(bytes.length);
|
||||||
|
buffer.write(bytes);
|
||||||
|
return Mono.just(buffer);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
private static class BeanWithNoDefaultConstructor {
|
||||||
|
|
||||||
|
private final String property1;
|
||||||
|
|
||||||
|
private final String property2;
|
||||||
|
|
||||||
|
public BeanWithNoDefaultConstructor(String property1, String property2) {
|
||||||
|
this.property1 = property1;
|
||||||
|
this.property2 = property2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getProperty1() {
|
||||||
|
return this.property1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getProperty2() {
|
||||||
|
return this.property2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@JsonDeserialize(using = Deserializer.class)
|
||||||
|
private static class TestObject {
|
||||||
|
|
||||||
|
private int test;
|
||||||
|
|
||||||
|
public int getTest() {
|
||||||
|
return this.test;
|
||||||
|
}
|
||||||
|
public void setTest(int test) {
|
||||||
|
this.test = test;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static class Deserializer extends StdDeserializer<TestObject> {
|
||||||
|
|
||||||
|
Deserializer() {
|
||||||
|
super(TestObject.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TestObject deserialize(JsonParser p, DeserializationContext ctxt) {
|
||||||
|
JsonNode node = p.readValueAsTree();
|
||||||
|
TestObject result = new TestObject();
|
||||||
|
result.setTest(node.get("test").asInt());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,268 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2025 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.json;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.time.Duration;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonTypeInfo;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonTypeName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.test.StepVerifier;
|
||||||
|
import tools.jackson.databind.ObjectMapper;
|
||||||
|
import tools.jackson.databind.SerializationFeature;
|
||||||
|
import tools.jackson.databind.json.JsonMapper;
|
||||||
|
|
||||||
|
import org.springframework.core.ResolvableType;
|
||||||
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
|
import org.springframework.core.testfixture.codec.AbstractEncoderTests;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.codec.json.JacksonViewBean.MyJacksonView1;
|
||||||
|
import org.springframework.http.codec.json.JacksonViewBean.MyJacksonView3;
|
||||||
|
import org.springframework.http.converter.json.MappingJacksonValue;
|
||||||
|
import org.springframework.util.MimeType;
|
||||||
|
import org.springframework.util.MimeTypeUtils;
|
||||||
|
import org.springframework.web.testfixture.xml.Pojo;
|
||||||
|
|
||||||
|
import static java.util.Collections.singletonMap;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
import static org.springframework.http.MediaType.APPLICATION_JSON;
|
||||||
|
import static org.springframework.http.MediaType.APPLICATION_NDJSON;
|
||||||
|
import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM;
|
||||||
|
import static org.springframework.http.MediaType.APPLICATION_XML;
|
||||||
|
import static org.springframework.http.codec.JacksonCodecSupport.JSON_VIEW_HINT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link JacksonJsonEncoder}.
|
||||||
|
* @author Sebastien Deleuze
|
||||||
|
*/
|
||||||
|
class JacksonJsonEncoderTests extends AbstractEncoderTests<JacksonJsonEncoder> {
|
||||||
|
|
||||||
|
public JacksonJsonEncoderTests() {
|
||||||
|
super(new JacksonJsonEncoder());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Test
|
||||||
|
public void canEncode() {
|
||||||
|
ResolvableType pojoType = ResolvableType.forClass(Pojo.class);
|
||||||
|
assertThat(this.encoder.canEncode(pojoType, APPLICATION_JSON)).isTrue();
|
||||||
|
assertThat(this.encoder.canEncode(pojoType, APPLICATION_NDJSON)).isTrue();
|
||||||
|
assertThat(this.encoder.canEncode(pojoType, null)).isTrue();
|
||||||
|
|
||||||
|
assertThat(this.encoder.canEncode(ResolvableType.forClass(Pojo.class),
|
||||||
|
new MediaType("application", "json", StandardCharsets.UTF_8))).isTrue();
|
||||||
|
assertThat(this.encoder.canEncode(ResolvableType.forClass(Pojo.class),
|
||||||
|
new MediaType("application", "json", StandardCharsets.US_ASCII))).isTrue();
|
||||||
|
assertThat(this.encoder.canEncode(ResolvableType.forClass(Pojo.class),
|
||||||
|
new MediaType("application", "json", StandardCharsets.ISO_8859_1))).isFalse();
|
||||||
|
|
||||||
|
// SPR-15464
|
||||||
|
assertThat(this.encoder.canEncode(ResolvableType.NONE, null)).isTrue();
|
||||||
|
|
||||||
|
// SPR-15910
|
||||||
|
assertThat(this.encoder.canEncode(ResolvableType.forClass(Object.class), APPLICATION_OCTET_STREAM)).isFalse();
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> this.encoder.canEncode(ResolvableType.forClass(MappingJacksonValue.class), APPLICATION_JSON))
|
||||||
|
.isInstanceOf(UnsupportedOperationException.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Test
|
||||||
|
public void encode() throws Exception {
|
||||||
|
Flux<Object> input = Flux.just(new Pojo("foo", "bar"),
|
||||||
|
new Pojo("foofoo", "barbar"),
|
||||||
|
new Pojo("foofoofoo", "barbarbar"));
|
||||||
|
|
||||||
|
testEncodeAll(input, ResolvableType.forClass(Pojo.class), APPLICATION_NDJSON, null, step -> step
|
||||||
|
.consumeNextWith(expectString("{\"bar\":\"bar\",\"foo\":\"foo\"}\n"))
|
||||||
|
.consumeNextWith(expectString("{\"bar\":\"barbar\",\"foo\":\"foofoo\"}\n"))
|
||||||
|
.consumeNextWith(expectString("{\"bar\":\"barbarbar\",\"foo\":\"foofoofoo\"}\n"))
|
||||||
|
.verifyComplete()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test // SPR-15866
|
||||||
|
public void canEncodeWithCustomMimeType() {
|
||||||
|
MimeType textJavascript = new MimeType("text", "javascript", StandardCharsets.UTF_8);
|
||||||
|
JacksonJsonEncoder encoder = new JacksonJsonEncoder(new ObjectMapper(), textJavascript);
|
||||||
|
|
||||||
|
assertThat(encoder.getEncodableMimeTypes()).isEqualTo(Collections.singletonList(textJavascript));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void encodableMimeTypesIsImmutable() {
|
||||||
|
MimeType textJavascript = new MimeType("text", "javascript", StandardCharsets.UTF_8);
|
||||||
|
JacksonJsonEncoder encoder = new JacksonJsonEncoder(new ObjectMapper(), textJavascript);
|
||||||
|
|
||||||
|
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() ->
|
||||||
|
encoder.getEncodableMimeTypes().add(new MimeType("text", "ecmascript")));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void canNotEncode() {
|
||||||
|
assertThat(this.encoder.canEncode(ResolvableType.forClass(String.class), null)).isFalse();
|
||||||
|
assertThat(this.encoder.canEncode(ResolvableType.forClass(Pojo.class), APPLICATION_XML)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void encodeNonStream() {
|
||||||
|
Flux<Pojo> input = Flux.just(
|
||||||
|
new Pojo("foo", "bar"),
|
||||||
|
new Pojo("foofoo", "barbar"),
|
||||||
|
new Pojo("foofoofoo", "barbarbar")
|
||||||
|
);
|
||||||
|
|
||||||
|
testEncode(input, Pojo.class, step -> step
|
||||||
|
.consumeNextWith(expectString("[{\"bar\":\"bar\",\"foo\":\"foo\"}"))
|
||||||
|
.consumeNextWith(expectString(",{\"bar\":\"barbar\",\"foo\":\"foofoo\"}"))
|
||||||
|
.consumeNextWith(expectString(",{\"bar\":\"barbarbar\",\"foo\":\"foofoofoo\"}"))
|
||||||
|
.consumeNextWith(expectString("]"))
|
||||||
|
.verifyComplete());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void encodeNonStreamEmpty() {
|
||||||
|
testEncode(Flux.empty(), Pojo.class, step -> step
|
||||||
|
.consumeNextWith(expectString("["))
|
||||||
|
.consumeNextWith(expectString("]"))
|
||||||
|
.verifyComplete());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test // gh-29038
|
||||||
|
void encodeNonStreamWithErrorAsFirstSignal() {
|
||||||
|
String message = "I'm a teapot";
|
||||||
|
Flux<Object> input = Flux.error(new IllegalStateException(message));
|
||||||
|
|
||||||
|
Flux<DataBuffer> output = this.encoder.encode(
|
||||||
|
input, this.bufferFactory, ResolvableType.forClass(Pojo.class), null, null);
|
||||||
|
|
||||||
|
StepVerifier.create(output).expectErrorMessage(message).verify();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void encodeWithType() {
|
||||||
|
Flux<ParentClass> input = Flux.just(new Foo(), new Bar());
|
||||||
|
|
||||||
|
testEncode(input, ParentClass.class, step -> step
|
||||||
|
.consumeNextWith(expectString("[{\"type\":\"foo\"}"))
|
||||||
|
.consumeNextWith(expectString(",{\"type\":\"bar\"}"))
|
||||||
|
.consumeNextWith(expectString("]"))
|
||||||
|
.verifyComplete());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Test // SPR-15727
|
||||||
|
public void encodeStreamWithCustomStreamingType() {
|
||||||
|
MediaType fooMediaType = new MediaType("application", "foo");
|
||||||
|
MediaType barMediaType = new MediaType("application", "bar");
|
||||||
|
this.encoder.setStreamingMediaTypes(Arrays.asList(fooMediaType, barMediaType));
|
||||||
|
Flux<Pojo> input = Flux.just(
|
||||||
|
new Pojo("foo", "bar"),
|
||||||
|
new Pojo("foofoo", "barbar"),
|
||||||
|
new Pojo("foofoofoo", "barbarbar")
|
||||||
|
);
|
||||||
|
|
||||||
|
testEncode(input, ResolvableType.forClass(Pojo.class), barMediaType, null, step -> step
|
||||||
|
.consumeNextWith(expectString("{\"bar\":\"bar\",\"foo\":\"foo\"}\n"))
|
||||||
|
.consumeNextWith(expectString("{\"bar\":\"barbar\",\"foo\":\"foofoo\"}\n"))
|
||||||
|
.consumeNextWith(expectString("{\"bar\":\"barbarbar\",\"foo\":\"foofoofoo\"}\n"))
|
||||||
|
.verifyComplete()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void fieldLevelJsonView() {
|
||||||
|
JacksonViewBean bean = new JacksonViewBean();
|
||||||
|
bean.setWithView1("with");
|
||||||
|
bean.setWithView2("with");
|
||||||
|
bean.setWithoutView("without");
|
||||||
|
Mono<JacksonViewBean> input = Mono.just(bean);
|
||||||
|
|
||||||
|
ResolvableType type = ResolvableType.forClass(JacksonViewBean.class);
|
||||||
|
Map<String, Object> hints = singletonMap(JSON_VIEW_HINT, MyJacksonView1.class);
|
||||||
|
|
||||||
|
testEncode(input, type, null, hints, step -> step
|
||||||
|
.consumeNextWith(expectString("{\"withView1\":\"with\"}"))
|
||||||
|
.verifyComplete()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void classLevelJsonView() {
|
||||||
|
JacksonViewBean bean = new JacksonViewBean();
|
||||||
|
bean.setWithView1("with");
|
||||||
|
bean.setWithView2("with");
|
||||||
|
bean.setWithoutView("without");
|
||||||
|
Mono<JacksonViewBean> input = Mono.just(bean);
|
||||||
|
|
||||||
|
ResolvableType type = ResolvableType.forClass(JacksonViewBean.class);
|
||||||
|
Map<String, Object> hints = singletonMap(JSON_VIEW_HINT, MyJacksonView3.class);
|
||||||
|
|
||||||
|
testEncode(input, type, null, hints, step -> step
|
||||||
|
.consumeNextWith(expectString("{\"withoutView\":\"without\"}"))
|
||||||
|
.verifyComplete()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test // gh-22771
|
||||||
|
public void encodeWithFlushAfterWriteOff() {
|
||||||
|
ObjectMapper mapper = JsonMapper.builder().configure(SerializationFeature.FLUSH_AFTER_WRITE_VALUE, false).build();
|
||||||
|
JacksonJsonEncoder encoder = new JacksonJsonEncoder(mapper);
|
||||||
|
|
||||||
|
Flux<DataBuffer> result = encoder.encode(Flux.just(new Pojo("foo", "bar")), this.bufferFactory,
|
||||||
|
ResolvableType.forClass(Pojo.class), MimeTypeUtils.APPLICATION_JSON, Collections.emptyMap());
|
||||||
|
|
||||||
|
StepVerifier.create(result)
|
||||||
|
.consumeNextWith(expectString("[{\"bar\":\"bar\",\"foo\":\"foo\"}"))
|
||||||
|
.consumeNextWith(expectString("]"))
|
||||||
|
.expectComplete()
|
||||||
|
.verify(Duration.ofSeconds(5));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void encodeAscii() {
|
||||||
|
Mono<Object> input = Mono.just(new Pojo("foo", "bar"));
|
||||||
|
MimeType mimeType = new MimeType("application", "json", StandardCharsets.US_ASCII);
|
||||||
|
|
||||||
|
testEncode(input, ResolvableType.forClass(Pojo.class), mimeType, null, step -> step
|
||||||
|
.consumeNextWith(expectString("{\"bar\":\"bar\",\"foo\":\"foo\"}"))
|
||||||
|
.verifyComplete()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
|
||||||
|
private static class ParentClass {
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonTypeName("foo")
|
||||||
|
private static class Foo extends ParentClass {
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonTypeName("bar")
|
||||||
|
private static class Bar extends ParentClass {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,100 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2025 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.smile;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import tools.jackson.dataformat.smile.SmileMapper;
|
||||||
|
|
||||||
|
import org.springframework.core.ResolvableType;
|
||||||
|
import org.springframework.core.io.buffer.DataBuffer;
|
||||||
|
import org.springframework.core.testfixture.codec.AbstractDecoderTests;
|
||||||
|
import org.springframework.util.MimeType;
|
||||||
|
import org.springframework.web.testfixture.xml.Pojo;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.springframework.http.MediaType.APPLICATION_JSON;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link JacksonSmileDecoder}.
|
||||||
|
*
|
||||||
|
* @author Sebastien Deleuze
|
||||||
|
*/
|
||||||
|
class JacksonSmileDecoderTests extends AbstractDecoderTests<JacksonSmileDecoder> {
|
||||||
|
|
||||||
|
private static final MimeType SMILE_MIME_TYPE = new MimeType("application", "x-jackson-smile");
|
||||||
|
private static final MimeType STREAM_SMILE_MIME_TYPE = new MimeType("application", "stream+x-jackson-smile");
|
||||||
|
|
||||||
|
private Pojo pojo1 = new Pojo("f1", "b1");
|
||||||
|
|
||||||
|
private Pojo pojo2 = new Pojo("f2", "b2");
|
||||||
|
|
||||||
|
private SmileMapper mapper = SmileMapper.builder().build();
|
||||||
|
|
||||||
|
public JacksonSmileDecoderTests() {
|
||||||
|
super(new JacksonSmileDecoder());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Test
|
||||||
|
protected void canDecode() {
|
||||||
|
assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), SMILE_MIME_TYPE)).isTrue();
|
||||||
|
assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), STREAM_SMILE_MIME_TYPE)).isTrue();
|
||||||
|
assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), null)).isTrue();
|
||||||
|
|
||||||
|
assertThat(decoder.canDecode(ResolvableType.forClass(String.class), null)).isFalse();
|
||||||
|
assertThat(decoder.canDecode(ResolvableType.forClass(Pojo.class), APPLICATION_JSON)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Test
|
||||||
|
protected void decode() {
|
||||||
|
Flux<DataBuffer> input = Flux.just(this.pojo1, this.pojo2)
|
||||||
|
.map(this::writeObject)
|
||||||
|
.flatMap(this::dataBuffer);
|
||||||
|
|
||||||
|
testDecodeAll(input, Pojo.class, step -> step
|
||||||
|
.expectNext(pojo1)
|
||||||
|
.expectNext(pojo2)
|
||||||
|
.verifyComplete());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte[] writeObject(Object o) {
|
||||||
|
return this.mapper.writer().writeValueAsBytes(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Test
|
||||||
|
protected void decodeToMono() {
|
||||||
|
List<Pojo> expected = Arrays.asList(pojo1, pojo2);
|
||||||
|
|
||||||
|
Flux<DataBuffer> input = Flux.just(expected)
|
||||||
|
.map(this::writeObject)
|
||||||
|
.flatMap(this::dataBuffer);
|
||||||
|
|
||||||
|
ResolvableType elementType = ResolvableType.forClassWithGenerics(List.class, Pojo.class);
|
||||||
|
testDecodeToMono(input, elementType, step -> step
|
||||||
|
.expectNext(expected)
|
||||||
|
.expectComplete()
|
||||||
|
.verify(), null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,130 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2002-2025 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.smile;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import reactor.core.publisher.Flux;
|
||||||
|
import reactor.core.publisher.Mono;
|
||||||
|
import reactor.test.StepVerifier;
|
||||||
|
import tools.jackson.databind.MappingIterator;
|
||||||
|
import tools.jackson.dataformat.smile.SmileMapper;
|
||||||
|
|
||||||
|
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.codec.ServerSentEvent;
|
||||||
|
import org.springframework.http.codec.json.Jackson2SmileEncoder;
|
||||||
|
import org.springframework.util.MimeType;
|
||||||
|
import org.springframework.web.testfixture.xml.Pojo;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.springframework.core.io.buffer.DataBufferUtils.release;
|
||||||
|
import static org.springframework.http.MediaType.APPLICATION_XML;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for {@link JacksonSmileEncoder}.
|
||||||
|
*
|
||||||
|
* @author Sebastien Deleuze
|
||||||
|
*/
|
||||||
|
class JacksonSmileEncoderTests extends AbstractEncoderTests<JacksonSmileEncoder> {
|
||||||
|
|
||||||
|
private static final MimeType SMILE_MIME_TYPE = new MimeType("application", "x-jackson-smile");
|
||||||
|
private static final MimeType STREAM_SMILE_MIME_TYPE = new MimeType("application", "stream+x-jackson-smile");
|
||||||
|
|
||||||
|
private final Jackson2SmileEncoder encoder = new Jackson2SmileEncoder();
|
||||||
|
|
||||||
|
private final SmileMapper mapper = SmileMapper.builder().build();
|
||||||
|
|
||||||
|
public JacksonSmileEncoderTests() {
|
||||||
|
super(new JacksonSmileEncoder());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Test
|
||||||
|
protected void canEncode() {
|
||||||
|
ResolvableType pojoType = ResolvableType.forClass(Pojo.class);
|
||||||
|
assertThat(this.encoder.canEncode(pojoType, SMILE_MIME_TYPE)).isTrue();
|
||||||
|
assertThat(this.encoder.canEncode(pojoType, STREAM_SMILE_MIME_TYPE)).isTrue();
|
||||||
|
assertThat(this.encoder.canEncode(pojoType, null)).isTrue();
|
||||||
|
|
||||||
|
// SPR-15464
|
||||||
|
assertThat(this.encoder.canEncode(ResolvableType.NONE, null)).isTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void canNotEncode() {
|
||||||
|
assertThat(this.encoder.canEncode(ResolvableType.forClass(String.class), null)).isFalse();
|
||||||
|
assertThat(this.encoder.canEncode(ResolvableType.forClass(Pojo.class), APPLICATION_XML)).isFalse();
|
||||||
|
|
||||||
|
ResolvableType sseType = ResolvableType.forClass(ServerSentEvent.class);
|
||||||
|
assertThat(this.encoder.canEncode(sseType, SMILE_MIME_TYPE)).isFalse();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Test
|
||||||
|
protected void encode() {
|
||||||
|
List<Pojo> list = Arrays.asList(
|
||||||
|
new Pojo("foo", "bar"),
|
||||||
|
new Pojo("foofoo", "barbar"),
|
||||||
|
new Pojo("foofoofoo", "barbarbar"));
|
||||||
|
|
||||||
|
Flux<Pojo> input = Flux.fromIterable(list);
|
||||||
|
|
||||||
|
testEncode(input, Pojo.class, step -> step
|
||||||
|
.consumeNextWith(dataBuffer -> {
|
||||||
|
try {
|
||||||
|
Object actual = this.mapper.reader().forType(List.class)
|
||||||
|
.readValue(dataBuffer.asInputStream());
|
||||||
|
assertThat(actual).isEqualTo(list);
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
release(dataBuffer);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void encodeError() {
|
||||||
|
Mono<Pojo> input = Mono.error(new InputException());
|
||||||
|
testEncode(input, Pojo.class, step -> step.expectError(InputException.class).verify());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void encodeAsStream() {
|
||||||
|
Pojo pojo1 = new Pojo("foo", "bar");
|
||||||
|
Pojo pojo2 = new Pojo("foofoo", "barbar");
|
||||||
|
Pojo pojo3 = new Pojo("foofoofoo", "barbarbar");
|
||||||
|
Flux<Pojo> input = Flux.just(pojo1, pojo2, pojo3);
|
||||||
|
ResolvableType type = ResolvableType.forClass(Pojo.class);
|
||||||
|
|
||||||
|
Flux<DataBuffer> result = this.encoder
|
||||||
|
.encode(input, bufferFactory, type, STREAM_SMILE_MIME_TYPE, null);
|
||||||
|
|
||||||
|
Mono<MappingIterator<Pojo>> joined = DataBufferUtils.join(result)
|
||||||
|
.map(buffer -> this.mapper.reader().forType(Pojo.class).readValues(buffer.asInputStream(true)));
|
||||||
|
|
||||||
|
StepVerifier.create(joined)
|
||||||
|
.assertNext(iter -> assertThat(iter).toIterable().contains(pojo1, pojo2, pojo3))
|
||||||
|
.verifyComplete();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -23,7 +23,6 @@ import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
|
|
||||||
|
@ -55,11 +54,8 @@ import org.springframework.http.codec.ResourceHttpMessageWriter;
|
||||||
import org.springframework.http.codec.ServerSentEventHttpMessageReader;
|
import org.springframework.http.codec.ServerSentEventHttpMessageReader;
|
||||||
import org.springframework.http.codec.cbor.KotlinSerializationCborDecoder;
|
import org.springframework.http.codec.cbor.KotlinSerializationCborDecoder;
|
||||||
import org.springframework.http.codec.cbor.KotlinSerializationCborEncoder;
|
import org.springframework.http.codec.cbor.KotlinSerializationCborEncoder;
|
||||||
import org.springframework.http.codec.json.Jackson2CodecSupport;
|
import org.springframework.http.codec.json.JacksonJsonDecoder;
|
||||||
import org.springframework.http.codec.json.Jackson2JsonDecoder;
|
import org.springframework.http.codec.json.JacksonJsonEncoder;
|
||||||
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.multipart.DefaultPartHttpMessageReader;
|
import org.springframework.http.codec.multipart.DefaultPartHttpMessageReader;
|
||||||
import org.springframework.http.codec.multipart.MultipartHttpMessageReader;
|
import org.springframework.http.codec.multipart.MultipartHttpMessageReader;
|
||||||
import org.springframework.http.codec.multipart.MultipartHttpMessageWriter;
|
import org.springframework.http.codec.multipart.MultipartHttpMessageWriter;
|
||||||
|
@ -70,6 +66,8 @@ import org.springframework.http.codec.protobuf.KotlinSerializationProtobufDecode
|
||||||
import org.springframework.http.codec.protobuf.KotlinSerializationProtobufEncoder;
|
import org.springframework.http.codec.protobuf.KotlinSerializationProtobufEncoder;
|
||||||
import org.springframework.http.codec.protobuf.ProtobufDecoder;
|
import org.springframework.http.codec.protobuf.ProtobufDecoder;
|
||||||
import org.springframework.http.codec.protobuf.ProtobufHttpMessageWriter;
|
import org.springframework.http.codec.protobuf.ProtobufHttpMessageWriter;
|
||||||
|
import org.springframework.http.codec.smile.JacksonSmileDecoder;
|
||||||
|
import org.springframework.http.codec.smile.JacksonSmileEncoder;
|
||||||
import org.springframework.http.codec.xml.Jaxb2XmlDecoder;
|
import org.springframework.http.codec.xml.Jaxb2XmlDecoder;
|
||||||
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
|
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
|
||||||
import org.springframework.util.MimeTypeUtils;
|
import org.springframework.util.MimeTypeUtils;
|
||||||
|
@ -81,6 +79,7 @@ import static org.springframework.core.ResolvableType.forClass;
|
||||||
* Tests for {@link ClientCodecConfigurer}.
|
* Tests for {@link ClientCodecConfigurer}.
|
||||||
*
|
*
|
||||||
* @author Rossen Stoyanchev
|
* @author Rossen Stoyanchev
|
||||||
|
* @author Sebastien Deleuze
|
||||||
*/
|
*/
|
||||||
class ClientCodecConfigurerTests {
|
class ClientCodecConfigurerTests {
|
||||||
|
|
||||||
|
@ -107,8 +106,8 @@ class ClientCodecConfigurerTests {
|
||||||
assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(PartEventHttpMessageReader.class);
|
assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(PartEventHttpMessageReader.class);
|
||||||
assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationCborDecoder.class);
|
assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationCborDecoder.class);
|
||||||
assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationProtobufDecoder.class);
|
assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationProtobufDecoder.class);
|
||||||
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2JsonDecoder.class);
|
assertThat(getNextDecoder(readers).getClass()).isEqualTo(JacksonJsonDecoder.class);
|
||||||
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2SmileDecoder.class);
|
assertThat(getNextDecoder(readers).getClass()).isEqualTo(JacksonSmileDecoder.class);
|
||||||
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jaxb2XmlDecoder.class);
|
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jaxb2XmlDecoder.class);
|
||||||
assertSseReader(readers);
|
assertSseReader(readers);
|
||||||
assertStringDecoder(getNextDecoder(readers), false);
|
assertStringDecoder(getNextDecoder(readers), false);
|
||||||
|
@ -130,55 +129,33 @@ class ClientCodecConfigurerTests {
|
||||||
assertThat(writers.get(this.index.getAndIncrement()).getClass()).isEqualTo(PartHttpMessageWriter.class);
|
assertThat(writers.get(this.index.getAndIncrement()).getClass()).isEqualTo(PartHttpMessageWriter.class);
|
||||||
assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationCborEncoder.class);
|
assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationCborEncoder.class);
|
||||||
assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationProtobufEncoder.class);
|
assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationProtobufEncoder.class);
|
||||||
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2JsonEncoder.class);
|
assertThat(getNextEncoder(writers).getClass()).isEqualTo(JacksonJsonEncoder.class);
|
||||||
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2SmileEncoder.class);
|
assertThat(getNextEncoder(writers).getClass()).isEqualTo(JacksonSmileEncoder.class);
|
||||||
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jaxb2XmlEncoder.class);
|
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jaxb2XmlEncoder.class);
|
||||||
assertStringEncoder(getNextEncoder(writers), false);
|
assertStringEncoder(getNextEncoder(writers), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void jackson2CodecCustomization() {
|
void jacksonCodecCustomization() {
|
||||||
Jackson2JsonDecoder decoder = new Jackson2JsonDecoder();
|
JacksonJsonDecoder decoder = new JacksonJsonDecoder();
|
||||||
Jackson2JsonEncoder encoder = new Jackson2JsonEncoder();
|
JacksonJsonEncoder encoder = new JacksonJsonEncoder();
|
||||||
this.configurer.defaultCodecs().jackson2JsonDecoder(decoder);
|
this.configurer.defaultCodecs().jacksonJsonDecoder(decoder);
|
||||||
this.configurer.defaultCodecs().jackson2JsonEncoder(encoder);
|
this.configurer.defaultCodecs().jacksonJsonEncoder(encoder);
|
||||||
|
|
||||||
List<HttpMessageReader<?>> readers = this.configurer.getReaders();
|
List<HttpMessageReader<?>> readers = this.configurer.getReaders();
|
||||||
Jackson2JsonDecoder actualDecoder = findCodec(readers, Jackson2JsonDecoder.class);
|
JacksonJsonDecoder actualDecoder = findCodec(readers, JacksonJsonDecoder.class);
|
||||||
assertThat(actualDecoder).isSameAs(decoder);
|
assertThat(actualDecoder).isSameAs(decoder);
|
||||||
assertThat(findCodec(readers, ServerSentEventHttpMessageReader.class).getDecoder()).isSameAs(decoder);
|
assertThat(findCodec(readers, ServerSentEventHttpMessageReader.class).getDecoder()).isSameAs(decoder);
|
||||||
|
|
||||||
List<HttpMessageWriter<?>> writers = this.configurer.getWriters();
|
List<HttpMessageWriter<?>> writers = this.configurer.getWriters();
|
||||||
Jackson2JsonEncoder actualEncoder = findCodec(writers, Jackson2JsonEncoder.class);
|
JacksonJsonEncoder actualEncoder = findCodec(writers, JacksonJsonEncoder.class);
|
||||||
assertThat(actualEncoder).isSameAs(encoder);
|
assertThat(actualEncoder).isSameAs(encoder);
|
||||||
|
|
||||||
MultipartHttpMessageWriter multipartWriter = findCodec(writers, MultipartHttpMessageWriter.class);
|
MultipartHttpMessageWriter multipartWriter = findCodec(writers, MultipartHttpMessageWriter.class);
|
||||||
actualEncoder = findCodec(multipartWriter.getPartWriters(), Jackson2JsonEncoder.class);
|
actualEncoder = findCodec(multipartWriter.getPartWriters(), JacksonJsonEncoder.class);
|
||||||
assertThat(actualEncoder).isSameAs(encoder);
|
assertThat(actualEncoder).isSameAs(encoder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
|
||||||
void objectMapperCustomization() {
|
|
||||||
ObjectMapper objectMapper = new ObjectMapper();
|
|
||||||
this.configurer.defaultCodecs().configureDefaultCodec(codec -> {
|
|
||||||
if (codec instanceof Jackson2CodecSupport) {
|
|
||||||
((Jackson2CodecSupport) codec).setObjectMapper(objectMapper);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
List<HttpMessageReader<?>> readers = this.configurer.getReaders();
|
|
||||||
Jackson2JsonDecoder actualDecoder = findCodec(readers, Jackson2JsonDecoder.class);
|
|
||||||
assertThat(actualDecoder.getObjectMapper()).isSameAs(objectMapper);
|
|
||||||
|
|
||||||
List<HttpMessageWriter<?>> writers = this.configurer.getWriters();
|
|
||||||
Jackson2JsonEncoder actualEncoder = findCodec(writers, Jackson2JsonEncoder.class);
|
|
||||||
assertThat(actualEncoder.getObjectMapper()).isSameAs(objectMapper);
|
|
||||||
|
|
||||||
MultipartHttpMessageWriter multipartWriter = findCodec(writers, MultipartHttpMessageWriter.class);
|
|
||||||
actualEncoder = findCodec(multipartWriter.getPartWriters(), Jackson2JsonEncoder.class);
|
|
||||||
assertThat(actualEncoder.getObjectMapper()).isSameAs(objectMapper);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void maxInMemorySize() {
|
void maxInMemorySize() {
|
||||||
int size = 99;
|
int size = 99;
|
||||||
|
@ -198,13 +175,13 @@ class ClientCodecConfigurerTests {
|
||||||
assertThat(((PartEventHttpMessageReader) nextReader(readers)).getMaxInMemorySize()).isEqualTo(size);
|
assertThat(((PartEventHttpMessageReader) nextReader(readers)).getMaxInMemorySize()).isEqualTo(size);
|
||||||
assertThat(((KotlinSerializationCborDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
|
assertThat(((KotlinSerializationCborDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
|
||||||
assertThat(((KotlinSerializationProtobufDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
|
assertThat(((KotlinSerializationProtobufDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
|
||||||
assertThat(((Jackson2JsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
|
assertThat(((JacksonJsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
|
||||||
assertThat(((Jackson2SmileDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
|
assertThat(((JacksonSmileDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
|
||||||
assertThat(((Jaxb2XmlDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
|
assertThat(((Jaxb2XmlDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
|
||||||
|
|
||||||
ServerSentEventHttpMessageReader reader = (ServerSentEventHttpMessageReader) nextReader(readers);
|
ServerSentEventHttpMessageReader reader = (ServerSentEventHttpMessageReader) nextReader(readers);
|
||||||
assertThat(reader.getMaxInMemorySize()).isEqualTo(size);
|
assertThat(reader.getMaxInMemorySize()).isEqualTo(size);
|
||||||
assertThat(((Jackson2JsonDecoder) reader.getDecoder()).getMaxInMemorySize()).isEqualTo(size);
|
assertThat(((JacksonJsonDecoder) reader.getDecoder()).getMaxInMemorySize()).isEqualTo(size);
|
||||||
|
|
||||||
assertThat(((StringDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
|
assertThat(((StringDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
|
||||||
}
|
}
|
||||||
|
@ -226,9 +203,9 @@ class ClientCodecConfigurerTests {
|
||||||
void clonedConfigurer() {
|
void clonedConfigurer() {
|
||||||
ClientCodecConfigurer clone = this.configurer.clone();
|
ClientCodecConfigurer clone = this.configurer.clone();
|
||||||
|
|
||||||
Jackson2JsonDecoder jackson2Decoder = new Jackson2JsonDecoder();
|
JacksonJsonDecoder jacksonDecoder = new JacksonJsonDecoder();
|
||||||
clone.defaultCodecs().serverSentEventDecoder(jackson2Decoder);
|
clone.defaultCodecs().serverSentEventDecoder(jacksonDecoder);
|
||||||
clone.defaultCodecs().multipartCodecs().encoder(new Jackson2SmileEncoder());
|
clone.defaultCodecs().multipartCodecs().encoder(new JacksonSmileEncoder());
|
||||||
clone.defaultCodecs().multipartCodecs().writer(new ResourceHttpMessageWriter());
|
clone.defaultCodecs().multipartCodecs().writer(new ResourceHttpMessageWriter());
|
||||||
|
|
||||||
// Clone has the customizations
|
// Clone has the customizations
|
||||||
|
@ -236,7 +213,7 @@ class ClientCodecConfigurerTests {
|
||||||
Decoder<?> sseDecoder = findCodec(clone.getReaders(), ServerSentEventHttpMessageReader.class).getDecoder();
|
Decoder<?> sseDecoder = findCodec(clone.getReaders(), ServerSentEventHttpMessageReader.class).getDecoder();
|
||||||
List<HttpMessageWriter<?>> writers = findCodec(clone.getWriters(), MultipartHttpMessageWriter.class).getPartWriters();
|
List<HttpMessageWriter<?>> writers = findCodec(clone.getWriters(), MultipartHttpMessageWriter.class).getPartWriters();
|
||||||
|
|
||||||
assertThat(sseDecoder).isSameAs(jackson2Decoder);
|
assertThat(sseDecoder).isSameAs(jacksonDecoder);
|
||||||
assertThat(writers).hasSize(2);
|
assertThat(writers).hasSize(2);
|
||||||
|
|
||||||
// Original does not have the customizations
|
// Original does not have the customizations
|
||||||
|
@ -244,7 +221,7 @@ class ClientCodecConfigurerTests {
|
||||||
sseDecoder = findCodec(this.configurer.getReaders(), ServerSentEventHttpMessageReader.class).getDecoder();
|
sseDecoder = findCodec(this.configurer.getReaders(), ServerSentEventHttpMessageReader.class).getDecoder();
|
||||||
writers = findCodec(this.configurer.getWriters(), MultipartHttpMessageWriter.class).getPartWriters();
|
writers = findCodec(this.configurer.getWriters(), MultipartHttpMessageWriter.class).getPartWriters();
|
||||||
|
|
||||||
assertThat(sseDecoder).isNotSameAs(jackson2Decoder);
|
assertThat(sseDecoder).isNotSameAs(jacksonDecoder);
|
||||||
assertThat(writers).hasSize(16);
|
assertThat(writers).hasSize(16);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -264,7 +241,7 @@ class ClientCodecConfigurerTests {
|
||||||
ClientCodecConfigurer clone = this.configurer.clone();
|
ClientCodecConfigurer clone = this.configurer.clone();
|
||||||
|
|
||||||
this.configurer.registerDefaults(false);
|
this.configurer.registerDefaults(false);
|
||||||
this.configurer.customCodecs().register(new Jackson2JsonEncoder());
|
this.configurer.customCodecs().register(new JacksonJsonEncoder());
|
||||||
|
|
||||||
List<HttpMessageWriter<?>> writers =
|
List<HttpMessageWriter<?>> writers =
|
||||||
findCodec(clone.getWriters(), MultipartHttpMessageWriter.class).getPartWriters();
|
findCodec(clone.getWriters(), MultipartHttpMessageWriter.class).getPartWriters();
|
||||||
|
@ -332,7 +309,7 @@ class ClientCodecConfigurerTests {
|
||||||
assertThat(reader.getClass()).isEqualTo(ServerSentEventHttpMessageReader.class);
|
assertThat(reader.getClass()).isEqualTo(ServerSentEventHttpMessageReader.class);
|
||||||
Decoder<?> decoder = ((ServerSentEventHttpMessageReader) reader).getDecoder();
|
Decoder<?> decoder = ((ServerSentEventHttpMessageReader) reader).getDecoder();
|
||||||
assertThat(decoder).isNotNull();
|
assertThat(decoder).isNotNull();
|
||||||
assertThat(decoder.getClass()).isEqualTo(Jackson2JsonDecoder.class);
|
assertThat(decoder.getClass()).isEqualTo(JacksonJsonDecoder.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,10 +49,8 @@ import org.springframework.http.codec.ServerSentEventHttpMessageReader;
|
||||||
import org.springframework.http.codec.ServerSentEventHttpMessageWriter;
|
import org.springframework.http.codec.ServerSentEventHttpMessageWriter;
|
||||||
import org.springframework.http.codec.cbor.KotlinSerializationCborDecoder;
|
import org.springframework.http.codec.cbor.KotlinSerializationCborDecoder;
|
||||||
import org.springframework.http.codec.cbor.KotlinSerializationCborEncoder;
|
import org.springframework.http.codec.cbor.KotlinSerializationCborEncoder;
|
||||||
import org.springframework.http.codec.json.Jackson2JsonDecoder;
|
import org.springframework.http.codec.json.JacksonJsonDecoder;
|
||||||
import org.springframework.http.codec.json.Jackson2JsonEncoder;
|
import org.springframework.http.codec.json.JacksonJsonEncoder;
|
||||||
import org.springframework.http.codec.json.Jackson2SmileDecoder;
|
|
||||||
import org.springframework.http.codec.json.Jackson2SmileEncoder;
|
|
||||||
import org.springframework.http.codec.multipart.DefaultPartHttpMessageReader;
|
import org.springframework.http.codec.multipart.DefaultPartHttpMessageReader;
|
||||||
import org.springframework.http.codec.multipart.MultipartHttpMessageReader;
|
import org.springframework.http.codec.multipart.MultipartHttpMessageReader;
|
||||||
import org.springframework.http.codec.multipart.MultipartHttpMessageWriter;
|
import org.springframework.http.codec.multipart.MultipartHttpMessageWriter;
|
||||||
|
@ -64,6 +62,8 @@ import org.springframework.http.codec.protobuf.KotlinSerializationProtobufEncode
|
||||||
import org.springframework.http.codec.protobuf.ProtobufDecoder;
|
import org.springframework.http.codec.protobuf.ProtobufDecoder;
|
||||||
import org.springframework.http.codec.protobuf.ProtobufEncoder;
|
import org.springframework.http.codec.protobuf.ProtobufEncoder;
|
||||||
import org.springframework.http.codec.protobuf.ProtobufHttpMessageWriter;
|
import org.springframework.http.codec.protobuf.ProtobufHttpMessageWriter;
|
||||||
|
import org.springframework.http.codec.smile.JacksonSmileDecoder;
|
||||||
|
import org.springframework.http.codec.smile.JacksonSmileEncoder;
|
||||||
import org.springframework.http.codec.xml.Jaxb2XmlDecoder;
|
import org.springframework.http.codec.xml.Jaxb2XmlDecoder;
|
||||||
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
|
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
|
||||||
import org.springframework.util.MimeTypeUtils;
|
import org.springframework.util.MimeTypeUtils;
|
||||||
|
@ -102,8 +102,8 @@ class CodecConfigurerTests {
|
||||||
assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(PartEventHttpMessageReader.class);
|
assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(PartEventHttpMessageReader.class);
|
||||||
assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationCborDecoder.class);
|
assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationCborDecoder.class);
|
||||||
assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationProtobufDecoder.class);
|
assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationProtobufDecoder.class);
|
||||||
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2JsonDecoder.class);
|
assertThat(getNextDecoder(readers).getClass()).isEqualTo(JacksonJsonDecoder.class);
|
||||||
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2SmileDecoder.class);
|
assertThat(getNextDecoder(readers).getClass()).isEqualTo(JacksonSmileDecoder.class);
|
||||||
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jaxb2XmlDecoder.class);
|
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jaxb2XmlDecoder.class);
|
||||||
assertStringDecoder(getNextDecoder(readers), false);
|
assertStringDecoder(getNextDecoder(readers), false);
|
||||||
}
|
}
|
||||||
|
@ -124,8 +124,8 @@ class CodecConfigurerTests {
|
||||||
assertThat(writers.get(index.getAndIncrement()).getClass()).isEqualTo(PartHttpMessageWriter.class);
|
assertThat(writers.get(index.getAndIncrement()).getClass()).isEqualTo(PartHttpMessageWriter.class);
|
||||||
assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationCborEncoder.class);
|
assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationCborEncoder.class);
|
||||||
assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationProtobufEncoder.class);
|
assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationProtobufEncoder.class);
|
||||||
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2JsonEncoder.class);
|
assertThat(getNextEncoder(writers).getClass()).isEqualTo(JacksonJsonEncoder.class);
|
||||||
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2SmileEncoder.class);
|
assertThat(getNextEncoder(writers).getClass()).isEqualTo(JacksonSmileEncoder.class);
|
||||||
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jaxb2XmlEncoder.class);
|
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jaxb2XmlEncoder.class);
|
||||||
assertStringEncoder(getNextEncoder(writers), false);
|
assertStringEncoder(getNextEncoder(writers), false);
|
||||||
}
|
}
|
||||||
|
@ -170,8 +170,8 @@ class CodecConfigurerTests {
|
||||||
assertThat(readers.get(this.index.getAndIncrement())).isSameAs(customReader2);
|
assertThat(readers.get(this.index.getAndIncrement())).isSameAs(customReader2);
|
||||||
assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationCborDecoder.class);
|
assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationCborDecoder.class);
|
||||||
assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationProtobufDecoder.class);
|
assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationProtobufDecoder.class);
|
||||||
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2JsonDecoder.class);
|
assertThat(getNextDecoder(readers).getClass()).isEqualTo(JacksonJsonDecoder.class);
|
||||||
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2SmileDecoder.class);
|
assertThat(getNextDecoder(readers).getClass()).isEqualTo(JacksonSmileDecoder.class);
|
||||||
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jaxb2XmlDecoder.class);
|
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jaxb2XmlDecoder.class);
|
||||||
assertThat(getNextDecoder(readers).getClass()).isEqualTo(StringDecoder.class);
|
assertThat(getNextDecoder(readers).getClass()).isEqualTo(StringDecoder.class);
|
||||||
}
|
}
|
||||||
|
@ -215,8 +215,8 @@ class CodecConfigurerTests {
|
||||||
assertThat(writers.get(this.index.getAndIncrement())).isSameAs(customWriter2);
|
assertThat(writers.get(this.index.getAndIncrement())).isSameAs(customWriter2);
|
||||||
assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationCborEncoder.class);
|
assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationCborEncoder.class);
|
||||||
assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationProtobufEncoder.class);
|
assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationProtobufEncoder.class);
|
||||||
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2JsonEncoder.class);
|
assertThat(getNextEncoder(writers).getClass()).isEqualTo(JacksonJsonEncoder.class);
|
||||||
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2SmileEncoder.class);
|
assertThat(getNextEncoder(writers).getClass()).isEqualTo(JacksonSmileEncoder.class);
|
||||||
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jaxb2XmlEncoder.class);
|
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jaxb2XmlEncoder.class);
|
||||||
assertThat(getNextEncoder(writers).getClass()).isEqualTo(CharSequenceEncoder.class);
|
assertThat(getNextEncoder(writers).getClass()).isEqualTo(CharSequenceEncoder.class);
|
||||||
}
|
}
|
||||||
|
@ -285,19 +285,19 @@ class CodecConfigurerTests {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void encoderDecoderOverrides() {
|
void encoderDecoderOverrides() {
|
||||||
Jackson2JsonDecoder jacksonDecoder = new Jackson2JsonDecoder();
|
JacksonJsonDecoder jacksonDecoder = new JacksonJsonDecoder();
|
||||||
Jackson2JsonEncoder jacksonEncoder = new Jackson2JsonEncoder();
|
JacksonJsonEncoder jacksonEncoder = new JacksonJsonEncoder();
|
||||||
Jackson2SmileDecoder smileDecoder = new Jackson2SmileDecoder();
|
JacksonSmileDecoder smileDecoder = new JacksonSmileDecoder();
|
||||||
Jackson2SmileEncoder smileEncoder = new Jackson2SmileEncoder();
|
JacksonSmileEncoder smileEncoder = new JacksonSmileEncoder();
|
||||||
ProtobufDecoder protobufDecoder = new ProtobufDecoder(ExtensionRegistry.newInstance());
|
ProtobufDecoder protobufDecoder = new ProtobufDecoder(ExtensionRegistry.newInstance());
|
||||||
ProtobufEncoder protobufEncoder = new ProtobufEncoder();
|
ProtobufEncoder protobufEncoder = new ProtobufEncoder();
|
||||||
Jaxb2XmlEncoder jaxb2Encoder = new Jaxb2XmlEncoder();
|
Jaxb2XmlEncoder jaxb2Encoder = new Jaxb2XmlEncoder();
|
||||||
Jaxb2XmlDecoder jaxb2Decoder = new Jaxb2XmlDecoder();
|
Jaxb2XmlDecoder jaxb2Decoder = new Jaxb2XmlDecoder();
|
||||||
|
|
||||||
this.configurer.defaultCodecs().jackson2JsonDecoder(jacksonDecoder);
|
this.configurer.defaultCodecs().jacksonJsonDecoder(jacksonDecoder);
|
||||||
this.configurer.defaultCodecs().jackson2JsonEncoder(jacksonEncoder);
|
this.configurer.defaultCodecs().jacksonJsonEncoder(jacksonEncoder);
|
||||||
this.configurer.defaultCodecs().jackson2SmileDecoder(smileDecoder);
|
this.configurer.defaultCodecs().jacksonSmileDecoder(smileDecoder);
|
||||||
this.configurer.defaultCodecs().jackson2SmileEncoder(smileEncoder);
|
this.configurer.defaultCodecs().jacksonSmileEncoder(smileEncoder);
|
||||||
this.configurer.defaultCodecs().protobufDecoder(protobufDecoder);
|
this.configurer.defaultCodecs().protobufDecoder(protobufDecoder);
|
||||||
this.configurer.defaultCodecs().protobufEncoder(protobufEncoder);
|
this.configurer.defaultCodecs().protobufEncoder(protobufEncoder);
|
||||||
this.configurer.defaultCodecs().jaxb2Decoder(jaxb2Decoder);
|
this.configurer.defaultCodecs().jaxb2Decoder(jaxb2Decoder);
|
||||||
|
@ -320,8 +320,8 @@ class CodecConfigurerTests {
|
||||||
assertThat(this.configurer.getWriters()).isEmpty();
|
assertThat(this.configurer.getWriters()).isEmpty();
|
||||||
|
|
||||||
CodecConfigurer clone = this.configurer.clone();
|
CodecConfigurer clone = this.configurer.clone();
|
||||||
clone.customCodecs().register(new Jackson2JsonEncoder());
|
clone.customCodecs().register(new JacksonJsonEncoder());
|
||||||
clone.customCodecs().register(new Jackson2JsonDecoder());
|
clone.customCodecs().register(new JacksonJsonDecoder());
|
||||||
clone.customCodecs().register(new ServerSentEventHttpMessageReader());
|
clone.customCodecs().register(new ServerSentEventHttpMessageReader());
|
||||||
clone.customCodecs().register(new ServerSentEventHttpMessageWriter());
|
clone.customCodecs().register(new ServerSentEventHttpMessageWriter());
|
||||||
|
|
||||||
|
@ -337,8 +337,8 @@ class CodecConfigurerTests {
|
||||||
assertThat(this.configurer.getReaders()).isEmpty();
|
assertThat(this.configurer.getReaders()).isEmpty();
|
||||||
assertThat(this.configurer.getWriters()).isEmpty();
|
assertThat(this.configurer.getWriters()).isEmpty();
|
||||||
|
|
||||||
this.configurer.customCodecs().register(new Jackson2JsonEncoder());
|
this.configurer.customCodecs().register(new JacksonJsonEncoder());
|
||||||
this.configurer.customCodecs().register(new Jackson2JsonDecoder());
|
this.configurer.customCodecs().register(new JacksonJsonDecoder());
|
||||||
this.configurer.customCodecs().register(new ServerSentEventHttpMessageReader());
|
this.configurer.customCodecs().register(new ServerSentEventHttpMessageReader());
|
||||||
this.configurer.customCodecs().register(new ServerSentEventHttpMessageWriter());
|
this.configurer.customCodecs().register(new ServerSentEventHttpMessageWriter());
|
||||||
assertThat(this.configurer.getReaders()).hasSize(2);
|
assertThat(this.configurer.getReaders()).hasSize(2);
|
||||||
|
@ -355,15 +355,15 @@ class CodecConfigurerTests {
|
||||||
void cloneDefaultCodecs() {
|
void cloneDefaultCodecs() {
|
||||||
CodecConfigurer clone = this.configurer.clone();
|
CodecConfigurer clone = this.configurer.clone();
|
||||||
|
|
||||||
Jackson2JsonDecoder jacksonDecoder = new Jackson2JsonDecoder();
|
JacksonJsonDecoder jacksonDecoder = new JacksonJsonDecoder();
|
||||||
Jackson2JsonEncoder jacksonEncoder = new Jackson2JsonEncoder();
|
JacksonJsonEncoder jacksonEncoder = new JacksonJsonEncoder();
|
||||||
Jaxb2XmlDecoder jaxb2Decoder = new Jaxb2XmlDecoder();
|
Jaxb2XmlDecoder jaxb2Decoder = new Jaxb2XmlDecoder();
|
||||||
Jaxb2XmlEncoder jaxb2Encoder = new Jaxb2XmlEncoder();
|
Jaxb2XmlEncoder jaxb2Encoder = new Jaxb2XmlEncoder();
|
||||||
ProtobufDecoder protoDecoder = new ProtobufDecoder();
|
ProtobufDecoder protoDecoder = new ProtobufDecoder();
|
||||||
ProtobufEncoder protoEncoder = new ProtobufEncoder();
|
ProtobufEncoder protoEncoder = new ProtobufEncoder();
|
||||||
|
|
||||||
clone.defaultCodecs().jackson2JsonDecoder(jacksonDecoder);
|
clone.defaultCodecs().jacksonJsonDecoder(jacksonDecoder);
|
||||||
clone.defaultCodecs().jackson2JsonEncoder(jacksonEncoder);
|
clone.defaultCodecs().jacksonJsonEncoder(jacksonEncoder);
|
||||||
clone.defaultCodecs().jaxb2Decoder(jaxb2Decoder);
|
clone.defaultCodecs().jaxb2Decoder(jaxb2Decoder);
|
||||||
clone.defaultCodecs().jaxb2Encoder(jaxb2Encoder);
|
clone.defaultCodecs().jaxb2Encoder(jaxb2Encoder);
|
||||||
clone.defaultCodecs().protobufDecoder(protoDecoder);
|
clone.defaultCodecs().protobufDecoder(protoDecoder);
|
||||||
|
|
|
@ -54,10 +54,8 @@ import org.springframework.http.codec.ServerCodecConfigurer;
|
||||||
import org.springframework.http.codec.ServerSentEventHttpMessageWriter;
|
import org.springframework.http.codec.ServerSentEventHttpMessageWriter;
|
||||||
import org.springframework.http.codec.cbor.KotlinSerializationCborDecoder;
|
import org.springframework.http.codec.cbor.KotlinSerializationCborDecoder;
|
||||||
import org.springframework.http.codec.cbor.KotlinSerializationCborEncoder;
|
import org.springframework.http.codec.cbor.KotlinSerializationCborEncoder;
|
||||||
import org.springframework.http.codec.json.Jackson2JsonDecoder;
|
import org.springframework.http.codec.json.JacksonJsonDecoder;
|
||||||
import org.springframework.http.codec.json.Jackson2JsonEncoder;
|
import org.springframework.http.codec.json.JacksonJsonEncoder;
|
||||||
import org.springframework.http.codec.json.Jackson2SmileDecoder;
|
|
||||||
import org.springframework.http.codec.json.Jackson2SmileEncoder;
|
|
||||||
import org.springframework.http.codec.multipart.DefaultPartHttpMessageReader;
|
import org.springframework.http.codec.multipart.DefaultPartHttpMessageReader;
|
||||||
import org.springframework.http.codec.multipart.MultipartHttpMessageReader;
|
import org.springframework.http.codec.multipart.MultipartHttpMessageReader;
|
||||||
import org.springframework.http.codec.multipart.MultipartHttpMessageWriter;
|
import org.springframework.http.codec.multipart.MultipartHttpMessageWriter;
|
||||||
|
@ -68,6 +66,8 @@ import org.springframework.http.codec.protobuf.KotlinSerializationProtobufDecode
|
||||||
import org.springframework.http.codec.protobuf.KotlinSerializationProtobufEncoder;
|
import org.springframework.http.codec.protobuf.KotlinSerializationProtobufEncoder;
|
||||||
import org.springframework.http.codec.protobuf.ProtobufDecoder;
|
import org.springframework.http.codec.protobuf.ProtobufDecoder;
|
||||||
import org.springframework.http.codec.protobuf.ProtobufHttpMessageWriter;
|
import org.springframework.http.codec.protobuf.ProtobufHttpMessageWriter;
|
||||||
|
import org.springframework.http.codec.smile.JacksonSmileDecoder;
|
||||||
|
import org.springframework.http.codec.smile.JacksonSmileEncoder;
|
||||||
import org.springframework.http.codec.xml.Jaxb2XmlDecoder;
|
import org.springframework.http.codec.xml.Jaxb2XmlDecoder;
|
||||||
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
|
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
|
||||||
import org.springframework.util.MimeTypeUtils;
|
import org.springframework.util.MimeTypeUtils;
|
||||||
|
@ -79,6 +79,7 @@ import static org.springframework.core.ResolvableType.forClass;
|
||||||
* Tests for {@link ServerCodecConfigurer}.
|
* Tests for {@link ServerCodecConfigurer}.
|
||||||
*
|
*
|
||||||
* @author Rossen Stoyanchev
|
* @author Rossen Stoyanchev
|
||||||
|
* @author Sebastien Deleuze
|
||||||
*/
|
*/
|
||||||
class ServerCodecConfigurerTests {
|
class ServerCodecConfigurerTests {
|
||||||
|
|
||||||
|
@ -104,8 +105,8 @@ class ServerCodecConfigurerTests {
|
||||||
assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(PartEventHttpMessageReader.class);
|
assertThat(readers.get(this.index.getAndIncrement()).getClass()).isEqualTo(PartEventHttpMessageReader.class);
|
||||||
assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationCborDecoder.class);
|
assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationCborDecoder.class);
|
||||||
assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationProtobufDecoder.class);
|
assertThat(getNextDecoder(readers).getClass()).isEqualTo(KotlinSerializationProtobufDecoder.class);
|
||||||
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2JsonDecoder.class);
|
assertThat(getNextDecoder(readers).getClass()).isEqualTo(JacksonJsonDecoder.class);
|
||||||
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jackson2SmileDecoder.class);
|
assertThat(getNextDecoder(readers).getClass()).isEqualTo(JacksonSmileDecoder.class);
|
||||||
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jaxb2XmlDecoder.class);
|
assertThat(getNextDecoder(readers).getClass()).isEqualTo(Jaxb2XmlDecoder.class);
|
||||||
assertStringDecoder(getNextDecoder(readers), false);
|
assertStringDecoder(getNextDecoder(readers), false);
|
||||||
}
|
}
|
||||||
|
@ -126,26 +127,26 @@ class ServerCodecConfigurerTests {
|
||||||
assertThat(writers.get(this.index.getAndIncrement()).getClass()).isEqualTo(PartHttpMessageWriter.class);
|
assertThat(writers.get(this.index.getAndIncrement()).getClass()).isEqualTo(PartHttpMessageWriter.class);
|
||||||
assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationCborEncoder.class);
|
assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationCborEncoder.class);
|
||||||
assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationProtobufEncoder.class);
|
assertThat(getNextEncoder(writers).getClass()).isEqualTo(KotlinSerializationProtobufEncoder.class);
|
||||||
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2JsonEncoder.class);
|
assertThat(getNextEncoder(writers).getClass()).isEqualTo(JacksonJsonEncoder.class);
|
||||||
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jackson2SmileEncoder.class);
|
assertThat(getNextEncoder(writers).getClass()).isEqualTo(JacksonSmileEncoder.class);
|
||||||
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jaxb2XmlEncoder.class);
|
assertThat(getNextEncoder(writers).getClass()).isEqualTo(Jaxb2XmlEncoder.class);
|
||||||
assertSseWriter(writers);
|
assertSseWriter(writers);
|
||||||
assertStringEncoder(getNextEncoder(writers), false);
|
assertStringEncoder(getNextEncoder(writers), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void jackson2EncoderOverride() {
|
void jacksonEncoderOverride() {
|
||||||
Jackson2JsonDecoder decoder = new Jackson2JsonDecoder();
|
JacksonJsonDecoder decoder = new JacksonJsonDecoder();
|
||||||
Jackson2JsonEncoder encoder = new Jackson2JsonEncoder();
|
JacksonJsonEncoder encoder = new JacksonJsonEncoder();
|
||||||
this.configurer.defaultCodecs().jackson2JsonDecoder(decoder);
|
this.configurer.defaultCodecs().jacksonJsonDecoder(decoder);
|
||||||
this.configurer.defaultCodecs().jackson2JsonEncoder(encoder);
|
this.configurer.defaultCodecs().jacksonJsonEncoder(encoder);
|
||||||
|
|
||||||
List<HttpMessageReader<?>> readers = this.configurer.getReaders();
|
List<HttpMessageReader<?>> readers = this.configurer.getReaders();
|
||||||
Jackson2JsonDecoder actualDecoder = findCodec(readers, Jackson2JsonDecoder.class);
|
JacksonJsonDecoder actualDecoder = findCodec(readers, JacksonJsonDecoder.class);
|
||||||
assertThat(actualDecoder).isSameAs(decoder);
|
assertThat(actualDecoder).isSameAs(decoder);
|
||||||
|
|
||||||
List<HttpMessageWriter<?>> writers = this.configurer.getWriters();
|
List<HttpMessageWriter<?>> writers = this.configurer.getWriters();
|
||||||
Jackson2JsonEncoder actualEncoder = findCodec(writers, Jackson2JsonEncoder.class);
|
JacksonJsonEncoder actualEncoder = findCodec(writers, JacksonJsonEncoder.class);
|
||||||
assertThat(actualEncoder).isSameAs(encoder);
|
assertThat(actualEncoder).isSameAs(encoder);
|
||||||
assertThat(findCodec(writers, ServerSentEventHttpMessageWriter.class).getEncoder()).isSameAs(encoder);
|
assertThat(findCodec(writers, ServerSentEventHttpMessageWriter.class).getEncoder()).isSameAs(encoder);
|
||||||
}
|
}
|
||||||
|
@ -173,8 +174,8 @@ class ServerCodecConfigurerTests {
|
||||||
|
|
||||||
assertThat(((KotlinSerializationCborDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
|
assertThat(((KotlinSerializationCborDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
|
||||||
assertThat(((KotlinSerializationProtobufDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
|
assertThat(((KotlinSerializationProtobufDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
|
||||||
assertThat(((Jackson2JsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
|
assertThat(((JacksonJsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
|
||||||
assertThat(((Jackson2SmileDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
|
assertThat(((JacksonSmileDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
|
||||||
assertThat(((Jaxb2XmlDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
|
assertThat(((Jaxb2XmlDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
|
||||||
assertThat(((StringDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
|
assertThat(((StringDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
|
||||||
}
|
}
|
||||||
|
@ -189,16 +190,16 @@ class ServerCodecConfigurerTests {
|
||||||
CodecConfigurer.CustomCodecs customCodecs = this.configurer.customCodecs();
|
CodecConfigurer.CustomCodecs customCodecs = this.configurer.customCodecs();
|
||||||
customCodecs.register(new ByteArrayDecoder());
|
customCodecs.register(new ByteArrayDecoder());
|
||||||
customCodecs.registerWithDefaultConfig(new ByteArrayDecoder());
|
customCodecs.registerWithDefaultConfig(new ByteArrayDecoder());
|
||||||
customCodecs.register(new Jackson2JsonDecoder());
|
customCodecs.register(new JacksonJsonDecoder());
|
||||||
customCodecs.registerWithDefaultConfig(new Jackson2JsonDecoder());
|
customCodecs.registerWithDefaultConfig(new JacksonJsonDecoder());
|
||||||
|
|
||||||
this.configurer.defaultCodecs().enableLoggingRequestDetails(true);
|
this.configurer.defaultCodecs().enableLoggingRequestDetails(true);
|
||||||
|
|
||||||
List<HttpMessageReader<?>> readers = this.configurer.getReaders();
|
List<HttpMessageReader<?>> readers = this.configurer.getReaders();
|
||||||
assertThat(((ByteArrayDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(256 * 1024);
|
assertThat(((ByteArrayDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(256 * 1024);
|
||||||
assertThat(((ByteArrayDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
|
assertThat(((ByteArrayDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
|
||||||
assertThat(((Jackson2JsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(256 * 1024);
|
assertThat(((JacksonJsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(256 * 1024);
|
||||||
assertThat(((Jackson2JsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
|
assertThat(((JacksonJsonDecoder) getNextDecoder(readers)).getMaxInMemorySize()).isEqualTo(size);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -235,7 +236,7 @@ class ServerCodecConfigurerTests {
|
||||||
ServerCodecConfigurer clone = this.configurer.clone();
|
ServerCodecConfigurer clone = this.configurer.clone();
|
||||||
|
|
||||||
MultipartHttpMessageReader reader = new MultipartHttpMessageReader(new DefaultPartHttpMessageReader());
|
MultipartHttpMessageReader reader = new MultipartHttpMessageReader(new DefaultPartHttpMessageReader());
|
||||||
Jackson2JsonEncoder encoder = new Jackson2JsonEncoder();
|
JacksonJsonEncoder encoder = new JacksonJsonEncoder();
|
||||||
clone.defaultCodecs().multipartReader(reader);
|
clone.defaultCodecs().multipartReader(reader);
|
||||||
clone.defaultCodecs().serverSentEventEncoder(encoder);
|
clone.defaultCodecs().serverSentEventEncoder(encoder);
|
||||||
|
|
||||||
|
@ -319,7 +320,7 @@ class ServerCodecConfigurerTests {
|
||||||
assertThat(writer.getClass()).isEqualTo(ServerSentEventHttpMessageWriter.class);
|
assertThat(writer.getClass()).isEqualTo(ServerSentEventHttpMessageWriter.class);
|
||||||
Encoder<?> encoder = ((ServerSentEventHttpMessageWriter) writer).getEncoder();
|
Encoder<?> encoder = ((ServerSentEventHttpMessageWriter) writer).getEncoder();
|
||||||
assertThat(encoder).isNotNull();
|
assertThat(encoder).isNotNull();
|
||||||
assertThat(encoder.getClass()).isEqualTo(Jackson2JsonEncoder.class);
|
assertThat(encoder.getClass()).isEqualTo(JacksonJsonEncoder.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,8 @@ dependencies {
|
||||||
optional("org.jetbrains.kotlin:kotlin-stdlib")
|
optional("org.jetbrains.kotlin:kotlin-stdlib")
|
||||||
optional("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
|
optional("org.jetbrains.kotlinx:kotlinx-coroutines-reactor")
|
||||||
optional("org.webjars:webjars-locator-lite")
|
optional("org.webjars:webjars-locator-lite")
|
||||||
|
optional("tools.jackson.core:jackson-databind")
|
||||||
|
optional("tools.jackson.dataformat:jackson-dataformat-smile")
|
||||||
testImplementation(testFixtures(project(":spring-beans")))
|
testImplementation(testFixtures(project(":spring-beans")))
|
||||||
testImplementation(testFixtures(project(":spring-core")))
|
testImplementation(testFixtures(project(":spring-core")))
|
||||||
testImplementation(testFixtures(project(":spring-web")))
|
testImplementation(testFixtures(project(":spring-web")))
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2023 the original author or authors.
|
* Copyright 2002-2025 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -33,7 +33,7 @@ import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.http.HttpStatusCode;
|
import org.springframework.http.HttpStatusCode;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseCookie;
|
import org.springframework.http.ResponseCookie;
|
||||||
import org.springframework.http.codec.json.Jackson2CodecSupport;
|
import org.springframework.http.codec.JacksonCodecSupport;
|
||||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||||
import org.springframework.util.MultiValueMap;
|
import org.springframework.util.MultiValueMap;
|
||||||
import org.springframework.web.reactive.function.BodyInserter;
|
import org.springframework.web.reactive.function.BodyInserter;
|
||||||
|
@ -292,7 +292,7 @@ public interface EntityResponse<T> extends ServerResponse {
|
||||||
Builder<T> contentType(MediaType contentType);
|
Builder<T> contentType(MediaType contentType);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a serialization hint like {@link Jackson2CodecSupport#JSON_VIEW_HINT} to
|
* Add a serialization hint like {@link JacksonCodecSupport#JSON_VIEW_HINT} to
|
||||||
* customize how the body will be serialized.
|
* customize how the body will be serialized.
|
||||||
* @param key the hint key
|
* @param key the hint key
|
||||||
* @param value the hint value
|
* @param value the hint value
|
||||||
|
|
|
@ -40,7 +40,7 @@ import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.http.HttpRange;
|
import org.springframework.http.HttpRange;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.codec.HttpMessageReader;
|
import org.springframework.http.codec.HttpMessageReader;
|
||||||
import org.springframework.http.codec.json.Jackson2CodecSupport;
|
import org.springframework.http.codec.JacksonCodecSupport;
|
||||||
import org.springframework.http.codec.multipart.Part;
|
import org.springframework.http.codec.multipart.Part;
|
||||||
import org.springframework.http.server.RequestPath;
|
import org.springframework.http.server.RequestPath;
|
||||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||||
|
@ -142,7 +142,7 @@ public interface ServerRequest {
|
||||||
/**
|
/**
|
||||||
* Extract the body with the given {@code BodyExtractor} and hints.
|
* Extract the body with the given {@code BodyExtractor} and hints.
|
||||||
* @param extractor the {@code BodyExtractor} that reads from the request
|
* @param extractor the {@code BodyExtractor} that reads from the request
|
||||||
* @param hints the map of hints like {@link Jackson2CodecSupport#JSON_VIEW_HINT}
|
* @param hints the map of hints like {@link JacksonCodecSupport#JSON_VIEW_HINT}
|
||||||
* to use to customize body extraction
|
* to use to customize body extraction
|
||||||
* @param <T> the type of the body returned
|
* @param <T> the type of the body returned
|
||||||
* @return the extracted body
|
* @return the extracted body
|
||||||
|
|
|
@ -40,7 +40,7 @@ import org.springframework.http.HttpStatusCode;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseCookie;
|
import org.springframework.http.ResponseCookie;
|
||||||
import org.springframework.http.codec.HttpMessageWriter;
|
import org.springframework.http.codec.HttpMessageWriter;
|
||||||
import org.springframework.http.codec.json.Jackson2CodecSupport;
|
import org.springframework.http.codec.JacksonCodecSupport;
|
||||||
import org.springframework.http.server.reactive.ServerHttpResponse;
|
import org.springframework.http.server.reactive.ServerHttpResponse;
|
||||||
import org.springframework.util.MultiValueMap;
|
import org.springframework.util.MultiValueMap;
|
||||||
import org.springframework.web.ErrorResponse;
|
import org.springframework.web.ErrorResponse;
|
||||||
|
@ -385,7 +385,7 @@ public interface ServerResponse {
|
||||||
BodyBuilder contentType(MediaType contentType);
|
BodyBuilder contentType(MediaType contentType);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a serialization hint like {@link Jackson2CodecSupport#JSON_VIEW_HINT}
|
* Add a serialization hint like {@link JacksonCodecSupport#JSON_VIEW_HINT}
|
||||||
* to customize how the body will be serialized.
|
* to customize how the body will be serialized.
|
||||||
* @param key the hint key
|
* @param key the hint key
|
||||||
* @param value the hint value
|
* @param value the hint value
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2024 the original author or authors.
|
* Copyright 2002-2025 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -121,11 +121,12 @@ public class DispatcherHandlerErrorTests {
|
||||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
||||||
assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_PROBLEM_JSON);
|
assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_PROBLEM_JSON);
|
||||||
assertThat(response.getBodyAsString().block()).isEqualTo("""
|
assertThat(response.getBodyAsString().block()).isEqualTo("""
|
||||||
{"type":"about:blank",\
|
{\
|
||||||
"title":"Not Found",\
|
|
||||||
"status":404,\
|
|
||||||
"detail":"No static resource non-existing.",\
|
"detail":"No static resource non-existing.",\
|
||||||
"instance":"/resources/non-existing"}\
|
"instance":"\\/resources\\/non-existing",\
|
||||||
|
"status":404,\
|
||||||
|
"title":"Not Found",\
|
||||||
|
"type":"about:blank"}\
|
||||||
""");
|
""");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2024 the original author or authors.
|
* Copyright 2002-2025 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -50,7 +50,7 @@ import org.springframework.http.ReactiveHttpInputMessage;
|
||||||
import org.springframework.http.codec.DecoderHttpMessageReader;
|
import org.springframework.http.codec.DecoderHttpMessageReader;
|
||||||
import org.springframework.http.codec.FormHttpMessageReader;
|
import org.springframework.http.codec.FormHttpMessageReader;
|
||||||
import org.springframework.http.codec.HttpMessageReader;
|
import org.springframework.http.codec.HttpMessageReader;
|
||||||
import org.springframework.http.codec.json.Jackson2JsonDecoder;
|
import org.springframework.http.codec.json.JacksonJsonDecoder;
|
||||||
import org.springframework.http.codec.multipart.DefaultPartHttpMessageReader;
|
import org.springframework.http.codec.multipart.DefaultPartHttpMessageReader;
|
||||||
import org.springframework.http.codec.multipart.FilePart;
|
import org.springframework.http.codec.multipart.FilePart;
|
||||||
import org.springframework.http.codec.multipart.FormFieldPart;
|
import org.springframework.http.codec.multipart.FormFieldPart;
|
||||||
|
@ -66,7 +66,7 @@ import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRe
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||||
import static org.assertj.core.api.InstanceOfAssertFactories.type;
|
import static org.assertj.core.api.InstanceOfAssertFactories.type;
|
||||||
import static org.springframework.http.codec.json.Jackson2CodecSupport.JSON_VIEW_HINT;
|
import static org.springframework.http.codec.JacksonCodecSupport.JSON_VIEW_HINT;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Arjen Poutsma
|
* @author Arjen Poutsma
|
||||||
|
@ -89,7 +89,7 @@ class BodyExtractorsTests {
|
||||||
messageReaders.add(new DecoderHttpMessageReader<>(new ByteBufferDecoder()));
|
messageReaders.add(new DecoderHttpMessageReader<>(new ByteBufferDecoder()));
|
||||||
messageReaders.add(new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes()));
|
messageReaders.add(new DecoderHttpMessageReader<>(StringDecoder.allMimeTypes()));
|
||||||
messageReaders.add(new DecoderHttpMessageReader<>(new Jaxb2XmlDecoder()));
|
messageReaders.add(new DecoderHttpMessageReader<>(new Jaxb2XmlDecoder()));
|
||||||
messageReaders.add(new DecoderHttpMessageReader<>(new Jackson2JsonDecoder()));
|
messageReaders.add(new DecoderHttpMessageReader<>(new JacksonJsonDecoder()));
|
||||||
messageReaders.add(new FormHttpMessageReader());
|
messageReaders.add(new FormHttpMessageReader());
|
||||||
DefaultPartHttpMessageReader partReader = new DefaultPartHttpMessageReader();
|
DefaultPartHttpMessageReader partReader = new DefaultPartHttpMessageReader();
|
||||||
messageReaders.add(partReader);
|
messageReaders.add(partReader);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2024 the original author or authors.
|
* Copyright 2002-2025 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -56,7 +56,7 @@ import org.springframework.http.codec.HttpMessageWriter;
|
||||||
import org.springframework.http.codec.ResourceHttpMessageWriter;
|
import org.springframework.http.codec.ResourceHttpMessageWriter;
|
||||||
import org.springframework.http.codec.ServerSentEvent;
|
import org.springframework.http.codec.ServerSentEvent;
|
||||||
import org.springframework.http.codec.ServerSentEventHttpMessageWriter;
|
import org.springframework.http.codec.ServerSentEventHttpMessageWriter;
|
||||||
import org.springframework.http.codec.json.Jackson2JsonEncoder;
|
import org.springframework.http.codec.json.JacksonJsonEncoder;
|
||||||
import org.springframework.http.codec.multipart.MultipartHttpMessageWriter;
|
import org.springframework.http.codec.multipart.MultipartHttpMessageWriter;
|
||||||
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
|
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
|
||||||
import org.springframework.http.server.reactive.ServerHttpRequest;
|
import org.springframework.http.server.reactive.ServerHttpRequest;
|
||||||
|
@ -69,7 +69,7 @@ import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRe
|
||||||
|
|
||||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.springframework.http.codec.json.Jackson2CodecSupport.JSON_VIEW_HINT;
|
import static org.springframework.http.codec.JacksonCodecSupport.JSON_VIEW_HINT;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Arjen Poutsma
|
* @author Arjen Poutsma
|
||||||
|
@ -89,7 +89,7 @@ class BodyInsertersTests {
|
||||||
messageWriters.add(new EncoderHttpMessageWriter<>(CharSequenceEncoder.textPlainOnly()));
|
messageWriters.add(new EncoderHttpMessageWriter<>(CharSequenceEncoder.textPlainOnly()));
|
||||||
messageWriters.add(new ResourceHttpMessageWriter());
|
messageWriters.add(new ResourceHttpMessageWriter());
|
||||||
messageWriters.add(new EncoderHttpMessageWriter<>(new Jaxb2XmlEncoder()));
|
messageWriters.add(new EncoderHttpMessageWriter<>(new Jaxb2XmlEncoder()));
|
||||||
Jackson2JsonEncoder jsonEncoder = new Jackson2JsonEncoder();
|
JacksonJsonEncoder jsonEncoder = new JacksonJsonEncoder();
|
||||||
messageWriters.add(new EncoderHttpMessageWriter<>(jsonEncoder));
|
messageWriters.add(new EncoderHttpMessageWriter<>(jsonEncoder));
|
||||||
messageWriters.add(new ServerSentEventHttpMessageWriter(jsonEncoder));
|
messageWriters.add(new ServerSentEventHttpMessageWriter(jsonEncoder));
|
||||||
messageWriters.add(new FormHttpMessageWriter());
|
messageWriters.add(new FormHttpMessageWriter());
|
||||||
|
@ -140,7 +140,7 @@ class BodyInsertersTests {
|
||||||
StepVerifier.create(result).expectComplete().verify();
|
StepVerifier.create(result).expectComplete().verify();
|
||||||
|
|
||||||
StepVerifier.create(response.getBodyAsString())
|
StepVerifier.create(response.getBodyAsString())
|
||||||
.expectNext("{\"username\":\"foo\",\"password\":\"bar\"}")
|
.expectNext("{\"password\":\"bar\",\"username\":\"foo\"}")
|
||||||
.expectComplete()
|
.expectComplete()
|
||||||
.verify();
|
.verify();
|
||||||
}
|
}
|
||||||
|
@ -169,7 +169,7 @@ class BodyInsertersTests {
|
||||||
Mono<Void> result = inserter.insert(response, this.context);
|
Mono<Void> result = inserter.insert(response, this.context);
|
||||||
StepVerifier.create(result).expectComplete().verify();
|
StepVerifier.create(result).expectComplete().verify();
|
||||||
StepVerifier.create(response.getBodyAsString())
|
StepVerifier.create(response.getBodyAsString())
|
||||||
.expectNext("{\"username\":\"foo\",\"password\":\"bar\"}")
|
.expectNext("{\"password\":\"bar\",\"username\":\"foo\"}")
|
||||||
.expectComplete()
|
.expectComplete()
|
||||||
.verify();
|
.verify();
|
||||||
}
|
}
|
||||||
|
@ -200,7 +200,7 @@ class BodyInsertersTests {
|
||||||
Mono<Void> result = inserter.insert(response, this.context);
|
Mono<Void> result = inserter.insert(response, this.context);
|
||||||
StepVerifier.create(result).expectComplete().verify();
|
StepVerifier.create(result).expectComplete().verify();
|
||||||
StepVerifier.create(response.getBodyAsString())
|
StepVerifier.create(response.getBodyAsString())
|
||||||
.expectNext("{\"username\":\"foo\",\"password\":\"bar\"}")
|
.expectNext("{\"password\":\"bar\",\"username\":\"foo\"}")
|
||||||
.expectComplete()
|
.expectComplete()
|
||||||
.verify();
|
.verify();
|
||||||
}
|
}
|
||||||
|
|
|
@ -767,7 +767,7 @@ class WebClientIntegrationTests {
|
||||||
expectRequestCount(1);
|
expectRequestCount(1);
|
||||||
expectRequest(request -> {
|
expectRequest(request -> {
|
||||||
assertThat(request.getPath()).isEqualTo("/pojo/capitalize");
|
assertThat(request.getPath()).isEqualTo("/pojo/capitalize");
|
||||||
assertThat(request.getBody().readUtf8()).isEqualTo("{\"foo\":\"foofoo\",\"bar\":\"barbar\"}");
|
assertThat(request.getBody().readUtf8()).isEqualTo("{\"bar\":\"barbar\",\"foo\":\"foofoo\"}");
|
||||||
assertThat(request.getHeader(HttpHeaders.CONTENT_LENGTH)).isEqualTo("31");
|
assertThat(request.getHeader(HttpHeaders.CONTENT_LENGTH)).isEqualTo("31");
|
||||||
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json");
|
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json");
|
||||||
assertThat(request.getHeader(HttpHeaders.CONTENT_TYPE)).isEqualTo("application/json");
|
assertThat(request.getHeader(HttpHeaders.CONTENT_TYPE)).isEqualTo("application/json");
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2002-2024 the original author or authors.
|
* Copyright 2002-2025 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
@ -122,11 +122,11 @@ class RequestMappingExceptionHandlingIntegrationTests extends AbstractRequestMap
|
||||||
.isThrownBy(() -> performGet("/no-such-handler", new HttpHeaders(), String.class))
|
.isThrownBy(() -> performGet("/no-such-handler", new HttpHeaders(), String.class))
|
||||||
.satisfies(ex -> {
|
.satisfies(ex -> {
|
||||||
assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
|
||||||
assertThat(ex.getResponseBodyAsString()).isEqualTo(
|
assertThat(ex.getResponseBodyAsString()).isEqualTo("{" +
|
||||||
"{\"type\":\"about:blank\"," +
|
"\"instance\":\"\\/no-such-handler\"," +
|
||||||
"\"title\":\"Not Found\"," +
|
|
||||||
"\"status\":404," +
|
"\"status\":404," +
|
||||||
"\"instance\":\"/no-such-handler\"}");
|
"\"title\":\"Not Found\"," +
|
||||||
|
"\"type\":\"about:blank\"}");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,11 +139,11 @@ class RequestMappingExceptionHandlingIntegrationTests extends AbstractRequestMap
|
||||||
.satisfies(ex -> {
|
.satisfies(ex -> {
|
||||||
assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
|
assertThat(ex.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
|
||||||
assertThat(ex.getResponseBodyAsString()).isEqualTo("{" +
|
assertThat(ex.getResponseBodyAsString()).isEqualTo("{" +
|
||||||
"\"type\":\"about:blank\"," +
|
|
||||||
"\"title\":\"Bad Request\"," +
|
|
||||||
"\"status\":400," +
|
|
||||||
"\"detail\":\"Required query parameter 'q' is not present.\"," +
|
"\"detail\":\"Required query parameter 'q' is not present.\"," +
|
||||||
"\"instance\":\"/missing-request-parameter\"}");
|
"\"instance\":\"\\/missing-request-parameter\"," +
|
||||||
|
"\"status\":400," +
|
||||||
|
"\"title\":\"Bad Request\"," +
|
||||||
|
"\"type\":\"about:blank\"}");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ import reactor.test.StepVerifier;
|
||||||
|
|
||||||
import org.springframework.core.codec.CharSequenceEncoder;
|
import org.springframework.core.codec.CharSequenceEncoder;
|
||||||
import org.springframework.http.MediaType;
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.codec.json.Jackson2JsonEncoder;
|
import org.springframework.http.codec.json.JacksonJsonEncoder;
|
||||||
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
|
import org.springframework.http.codec.xml.Jaxb2XmlEncoder;
|
||||||
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest;
|
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest;
|
||||||
import org.springframework.web.testfixture.server.MockServerWebExchange;
|
import org.springframework.web.testfixture.server.MockServerWebExchange;
|
||||||
|
@ -43,7 +43,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||||
*/
|
*/
|
||||||
class HttpMessageWriterViewTests {
|
class HttpMessageWriterViewTests {
|
||||||
|
|
||||||
private HttpMessageWriterView view = new HttpMessageWriterView(new Jackson2JsonEncoder());
|
private HttpMessageWriterView view = new HttpMessageWriterView(new JacksonJsonEncoder());
|
||||||
|
|
||||||
private final Map<String, Object> model = new HashMap<>();
|
private final Map<String, Object> model = new HashMap<>();
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ import org.springframework.http.MediaType
|
||||||
import org.springframework.http.ResponseEntity
|
import org.springframework.http.ResponseEntity
|
||||||
import org.springframework.http.codec.EncoderHttpMessageWriter
|
import org.springframework.http.codec.EncoderHttpMessageWriter
|
||||||
import org.springframework.http.codec.HttpMessageWriter
|
import org.springframework.http.codec.HttpMessageWriter
|
||||||
import org.springframework.http.codec.json.Jackson2JsonEncoder
|
import org.springframework.http.codec.json.JacksonJsonEncoder
|
||||||
import org.springframework.http.codec.json.KotlinSerializationJsonEncoder
|
import org.springframework.http.codec.json.KotlinSerializationJsonEncoder
|
||||||
import org.springframework.util.ObjectUtils
|
import org.springframework.util.ObjectUtils
|
||||||
import org.springframework.web.bind.annotation.GetMapping
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
|
@ -54,7 +54,7 @@ class MessageWriterResultHandlerKotlinTests {
|
||||||
val writerList = if (ObjectUtils.isEmpty(writers)) {
|
val writerList = if (ObjectUtils.isEmpty(writers)) {
|
||||||
listOf(
|
listOf(
|
||||||
EncoderHttpMessageWriter(KotlinSerializationJsonEncoder()),
|
EncoderHttpMessageWriter(KotlinSerializationJsonEncoder()),
|
||||||
EncoderHttpMessageWriter(Jackson2JsonEncoder())
|
EncoderHttpMessageWriter(JacksonJsonEncoder())
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
listOf(*writers)
|
listOf(*writers)
|
||||||
|
|
Loading…
Reference in New Issue