Browse Source
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-33798pull/34893/head
44 changed files with 3830 additions and 168 deletions
@ -0,0 +1,298 @@
@@ -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 @@
@@ -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; |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,275 @@
@@ -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 @@
@@ -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); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,76 @@
@@ -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 @@
@@ -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"); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,114 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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; |
||||
@ -0,0 +1,376 @@
@@ -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; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,103 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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(); |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue