Browse Source
This commit introduces Jackson 3 SmartHttpMessageConverter based variants of the following Jackson 2 classes (and related dependent classes). org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter -> org.springframework.http.converter.AbstractJacksonHttpMessageConverter MappingJackson2HttpMessageConverter -> JacksonJsonHttpMessageConverter MappingJackson2SmileHttpMessageConverter -> JacksonSmileHttpMessageConverter MappingJackson2CborHttpMessageConverter -> JacksonCborHttpMessageConverter MappingJackson2XmlHttpMessageConverter -> JacksonXmlHttpMessageConverter MappingJackson2YamlHttpMessageConverter -> JacksonYamlHttpMessageConverter They use hints instead of MappingJacksonValue and MappingJacksonInputMessage to support `@JsonView` and FilterProvider. Jackson 3 support is configured if found in the classpath otherwise fallback to Jackson 2. JacksonHandlerInstantiator needs to be enabled explicitly if needed. See gh-33798pull/34893/head
23 changed files with 3028 additions and 86 deletions
@ -0,0 +1,509 @@
@@ -0,0 +1,509 @@
|
||||
/* |
||||
* 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.converter; |
||||
|
||||
import java.io.IOException; |
||||
import java.io.InputStream; |
||||
import java.io.InputStreamReader; |
||||
import java.io.OutputStream; |
||||
import java.io.Reader; |
||||
import java.lang.reflect.Type; |
||||
import java.nio.charset.Charset; |
||||
import java.nio.charset.StandardCharsets; |
||||
import java.util.ArrayList; |
||||
import java.util.Arrays; |
||||
import java.util.Collections; |
||||
import java.util.LinkedHashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Objects; |
||||
import java.util.Optional; |
||||
import java.util.function.Consumer; |
||||
|
||||
import com.fasterxml.jackson.annotation.JsonView; |
||||
import org.jspecify.annotations.Nullable; |
||||
import tools.jackson.core.JacksonException; |
||||
import tools.jackson.core.JsonEncoding; |
||||
import tools.jackson.core.JsonGenerator; |
||||
import tools.jackson.core.PrettyPrinter; |
||||
import tools.jackson.core.util.DefaultIndenter; |
||||
import tools.jackson.core.util.DefaultPrettyPrinter; |
||||
import tools.jackson.databind.JacksonModule; |
||||
import tools.jackson.databind.JavaType; |
||||
import tools.jackson.databind.ObjectMapper; |
||||
import tools.jackson.databind.ObjectReader; |
||||
import tools.jackson.databind.ObjectWriter; |
||||
import tools.jackson.databind.SerializationConfig; |
||||
import tools.jackson.databind.SerializationFeature; |
||||
import tools.jackson.databind.cfg.MapperBuilder; |
||||
import tools.jackson.databind.exc.InvalidDefinitionException; |
||||
import tools.jackson.databind.ser.FilterProvider; |
||||
|
||||
import org.springframework.core.GenericTypeResolver; |
||||
import org.springframework.core.MethodParameter; |
||||
import org.springframework.core.ResolvableType; |
||||
import org.springframework.http.HttpInputMessage; |
||||
import org.springframework.http.HttpOutputMessage; |
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.http.ProblemDetail; |
||||
import org.springframework.http.converter.json.JacksonJsonHttpMessageConverter; |
||||
import org.springframework.http.converter.json.MappingJacksonInputMessage; |
||||
import org.springframework.http.converter.json.MappingJacksonValue; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.CollectionUtils; |
||||
import org.springframework.util.StreamUtils; |
||||
import org.springframework.util.TypeUtils; |
||||
|
||||
/** |
||||
* Abstract base class for Jackson based and content type independent |
||||
* {@link HttpMessageConverter} implementations. |
||||
* |
||||
* <p>The following hint entries are supported: |
||||
* <ul> |
||||
* <li>A JSON view with a <code>com.fasterxml.jackson.annotation.JsonView</code> |
||||
* key and the class name of the JSON view as value.</li> |
||||
* <li>A filter provider with a <code>tools.jackson.databind.ser.FilterProvider</code> |
||||
* key and the filter provider class name as value.</li> |
||||
* </ul> |
||||
* |
||||
* @author Sebastien Deleuze |
||||
* @since 7.0 |
||||
* @see JacksonJsonHttpMessageConverter |
||||
*/ |
||||
public abstract class AbstractJacksonHttpMessageConverter extends AbstractSmartHttpMessageConverter<Object> { |
||||
|
||||
private static final String JSON_VIEW_HINT = JsonView.class.getName(); |
||||
|
||||
private static final String FILTER_PROVIDER_HINT = FilterProvider.class.getName(); |
||||
|
||||
private static final Map<String, JsonEncoding> ENCODINGS; |
||||
|
||||
private static volatile @Nullable List<JacksonModule> modules = null; |
||||
|
||||
static { |
||||
ENCODINGS = CollectionUtils.newHashMap(JsonEncoding.values().length); |
||||
for (JsonEncoding encoding : JsonEncoding.values()) { |
||||
ENCODINGS.put(encoding.getJavaName(), encoding); |
||||
} |
||||
ENCODINGS.put("US-ASCII", JsonEncoding.UTF8); |
||||
} |
||||
|
||||
|
||||
protected final ObjectMapper defaultObjectMapper; |
||||
|
||||
private @Nullable Map<Class<?>, Map<MediaType, ObjectMapper>> objectMapperRegistrations; |
||||
|
||||
private final @Nullable PrettyPrinter ssePrettyPrinter; |
||||
|
||||
|
||||
/** |
||||
* Construct a new instance with a provided {@link MapperBuilder builder} |
||||
* customized with the {@link tools.jackson.databind.JacksonModule}s found |
||||
* by {@link MapperBuilder#findModules(ClassLoader)}. |
||||
*/ |
||||
private AbstractJacksonHttpMessageConverter(MapperBuilder<?, ?> builder) { |
||||
this.defaultObjectMapper = builder.addModules(initModules()).build(); |
||||
this.ssePrettyPrinter = initSsePrettyPrinter(); |
||||
} |
||||
|
||||
/** |
||||
* 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 MediaType}. |
||||
*/ |
||||
protected AbstractJacksonHttpMessageConverter(MapperBuilder<?, ?> builder, MediaType supportedMediaType) { |
||||
this(builder); |
||||
setSupportedMediaTypes(Collections.singletonList(supportedMediaType)); |
||||
} |
||||
|
||||
/** |
||||
* 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 MediaType}s. |
||||
*/ |
||||
protected AbstractJacksonHttpMessageConverter(MapperBuilder<?, ?> builder, MediaType... supportedMediaTypes) { |
||||
this(builder); |
||||
setSupportedMediaTypes(Arrays.asList(supportedMediaTypes)); |
||||
} |
||||
|
||||
/** |
||||
* Construct a new instance with a provided {@link ObjectMapper}. |
||||
*/ |
||||
protected AbstractJacksonHttpMessageConverter(ObjectMapper objectMapper) { |
||||
this.defaultObjectMapper = objectMapper; |
||||
this.ssePrettyPrinter = initSsePrettyPrinter(); |
||||
} |
||||
|
||||
/** |
||||
* Construct a new instance with the provided {@link ObjectMapper} and {@link MediaType}. |
||||
*/ |
||||
protected AbstractJacksonHttpMessageConverter(ObjectMapper objectMapper, MediaType supportedMediaType) { |
||||
this(objectMapper); |
||||
setSupportedMediaTypes(Collections.singletonList(supportedMediaType)); |
||||
} |
||||
|
||||
/** |
||||
* Construct a new instance with the provided {@link ObjectMapper} and {@link MediaType}s. |
||||
*/ |
||||
protected AbstractJacksonHttpMessageConverter(ObjectMapper objectMapper, MediaType... supportedMediaTypes) { |
||||
this(objectMapper); |
||||
setSupportedMediaTypes(Arrays.asList(supportedMediaTypes)); |
||||
} |
||||
|
||||
private List<JacksonModule> initModules() { |
||||
if (modules == null) { |
||||
modules = MapperBuilder.findModules(AbstractJacksonHttpMessageConverter.class.getClassLoader()); |
||||
|
||||
} |
||||
return Objects.requireNonNull(modules); |
||||
} |
||||
|
||||
private PrettyPrinter initSsePrettyPrinter() { |
||||
DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter(); |
||||
prettyPrinter.indentObjectsWith(new DefaultIndenter(" ", "\ndata:")); |
||||
return prettyPrinter; |
||||
} |
||||
|
||||
@Override |
||||
public void setSupportedMediaTypes(List<MediaType> supportedMediaTypes) { |
||||
super.setSupportedMediaTypes(supportedMediaTypes); |
||||
} |
||||
|
||||
/** |
||||
* Return the main {@code ObjectMapper} in use. |
||||
*/ |
||||
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 |
||||
* {@link #setSupportedMediaTypes(List) supportedMediaTypes} 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<MediaType, ObjectMapper>> registrar) { |
||||
if (this.objectMapperRegistrations == null) { |
||||
this.objectMapperRegistrations = new LinkedHashMap<>(); |
||||
} |
||||
Map<MediaType, 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 Map<MediaType, ObjectMapper> getObjectMappersForType(Class<?> clazz) { |
||||
for (Map.Entry<Class<?>, Map<MediaType, ObjectMapper>> entry : getObjectMapperRegistrations().entrySet()) { |
||||
if (entry.getKey().isAssignableFrom(clazz)) { |
||||
return entry.getValue(); |
||||
} |
||||
} |
||||
return Collections.emptyMap(); |
||||
} |
||||
|
||||
@Override |
||||
public List<MediaType> getSupportedMediaTypes(Class<?> clazz) { |
||||
List<MediaType> result = null; |
||||
for (Map.Entry<Class<?>, Map<MediaType, ObjectMapper>> entry : getObjectMapperRegistrations().entrySet()) { |
||||
if (entry.getKey().isAssignableFrom(clazz)) { |
||||
result = (result != null ? result : new ArrayList<>(entry.getValue().size())); |
||||
result.addAll(entry.getValue().keySet()); |
||||
} |
||||
} |
||||
if (!CollectionUtils.isEmpty(result)) { |
||||
return result; |
||||
} |
||||
return (ProblemDetail.class.isAssignableFrom(clazz) ? |
||||
getMediaTypesForProblemDetail() : getSupportedMediaTypes()); |
||||
} |
||||
|
||||
private Map<Class<?>, Map<MediaType, ObjectMapper>> getObjectMapperRegistrations() { |
||||
return (this.objectMapperRegistrations != null ? this.objectMapperRegistrations : Collections.emptyMap()); |
||||
} |
||||
|
||||
/** |
||||
* Return the supported media type(s) for {@link ProblemDetail}. |
||||
* By default, an empty list, unless overridden in subclasses. |
||||
*/ |
||||
protected List<MediaType> getMediaTypesForProblemDetail() { |
||||
return Collections.emptyList(); |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public boolean canRead(ResolvableType type, @Nullable MediaType mediaType) { |
||||
if (!canRead(mediaType)) { |
||||
return false; |
||||
} |
||||
Class<?> clazz = type.resolve(); |
||||
if (clazz == null) { |
||||
return false; |
||||
} |
||||
return this.objectMapperRegistrations == null || selectObjectMapper(clazz, mediaType) != null; |
||||
} |
||||
|
||||
@Override |
||||
public boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType) { |
||||
if (!canWrite(mediaType)) { |
||||
return false; |
||||
} |
||||
if (mediaType != null && mediaType.getCharset() != null) { |
||||
Charset charset = mediaType.getCharset(); |
||||
if (!ENCODINGS.containsKey(charset.name())) { |
||||
return false; |
||||
} |
||||
} |
||||
if (MappingJacksonValue.class.isAssignableFrom(clazz)) { |
||||
throw new UnsupportedOperationException("MappingJacksonValue is not supported, use hints instead"); |
||||
} |
||||
return this.objectMapperRegistrations == null || selectObjectMapper(clazz, mediaType) != null; |
||||
} |
||||
|
||||
/** |
||||
* 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)}. |
||||
*/ |
||||
private @Nullable ObjectMapper selectObjectMapper(Class<?> targetType, @Nullable MediaType targetMediaType) { |
||||
if (targetMediaType == null || CollectionUtils.isEmpty(this.objectMapperRegistrations)) { |
||||
return this.defaultObjectMapper; |
||||
} |
||||
for (Map.Entry<Class<?>, Map<MediaType, ObjectMapper>> typeEntry : getObjectMapperRegistrations().entrySet()) { |
||||
if (typeEntry.getKey().isAssignableFrom(targetType)) { |
||||
for (Map.Entry<MediaType, ObjectMapper> objectMapperEntry : typeEntry.getValue().entrySet()) { |
||||
if (objectMapperEntry.getKey().includes(targetMediaType)) { |
||||
return objectMapperEntry.getValue(); |
||||
} |
||||
} |
||||
// No matching registrations
|
||||
return null; |
||||
} |
||||
} |
||||
// No registrations
|
||||
return this.defaultObjectMapper; |
||||
} |
||||
|
||||
@Override |
||||
public Object read(ResolvableType type, HttpInputMessage inputMessage, @Nullable Map<String, Object> hints) |
||||
throws IOException, HttpMessageNotReadableException { |
||||
|
||||
Class<?> contextClass = (type.getSource() instanceof MethodParameter parameter ? parameter.getContainingClass() : null); |
||||
JavaType javaType = getJavaType(type.getType(), contextClass); |
||||
return readJavaType(javaType, inputMessage, hints); |
||||
} |
||||
|
||||
@Override |
||||
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) |
||||
throws IOException, HttpMessageNotReadableException { |
||||
|
||||
JavaType javaType = getJavaType(clazz, null); |
||||
return readJavaType(javaType, inputMessage, null); |
||||
} |
||||
|
||||
private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage, @Nullable Map<String, Object> hints) throws IOException { |
||||
MediaType contentType = inputMessage.getHeaders().getContentType(); |
||||
Charset charset = getCharset(contentType); |
||||
|
||||
ObjectMapper objectMapper = selectObjectMapper(javaType.getRawClass(), contentType); |
||||
Assert.state(objectMapper != null, () -> "No ObjectMapper for " + javaType); |
||||
|
||||
boolean isUnicode = ENCODINGS.containsKey(charset.name()) || |
||||
"UTF-16".equals(charset.name()) || |
||||
"UTF-32".equals(charset.name()); |
||||
try { |
||||
InputStream inputStream = StreamUtils.nonClosing(inputMessage.getBody()); |
||||
if (inputMessage instanceof MappingJacksonInputMessage) { |
||||
throw new UnsupportedOperationException("MappingJacksonInputMessage is not supported, use hints instead"); |
||||
} |
||||
ObjectReader objectReader = objectMapper.readerFor(javaType); |
||||
if (hints != null && hints.containsKey(JSON_VIEW_HINT)) { |
||||
objectReader = objectReader.withView((Class<?>) hints.get(JSON_VIEW_HINT)); |
||||
} |
||||
objectReader = customizeReader(objectReader, javaType); |
||||
if (isUnicode) { |
||||
return objectReader.readValue(inputStream); |
||||
} |
||||
else { |
||||
Reader reader = new InputStreamReader(inputStream, charset); |
||||
return objectReader.readValue(reader); |
||||
} |
||||
} |
||||
catch (InvalidDefinitionException ex) { |
||||
throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex); |
||||
} |
||||
catch (JacksonException ex) { |
||||
throw new HttpMessageNotReadableException("JSON parse error: " + ex.getOriginalMessage(), ex, inputMessage); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Subclasses can use this method to customize {@link ObjectReader} used |
||||
* for reading values. |
||||
* @param reader the reader instance to customize |
||||
* @param javaType the target type of element values to read to |
||||
* @return the customized {@link ObjectReader} |
||||
*/ |
||||
protected ObjectReader customizeReader(ObjectReader reader, JavaType javaType) { |
||||
return reader; |
||||
} |
||||
|
||||
/** |
||||
* Determine the charset to use for JSON input. |
||||
* <p>By default this is either the charset from the input {@code MediaType} |
||||
* or otherwise falling back on {@code UTF-8}. Can be overridden in subclasses. |
||||
* @param contentType the content type of the HTTP input message |
||||
* @return the charset to use |
||||
*/ |
||||
protected Charset getCharset(@Nullable MediaType contentType) { |
||||
if (contentType != null && contentType.getCharset() != null) { |
||||
return contentType.getCharset(); |
||||
} |
||||
else { |
||||
return StandardCharsets.UTF_8; |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
protected void writeInternal(Object object, ResolvableType resolvableType, HttpOutputMessage outputMessage, @Nullable Map<String, Object> hints) |
||||
throws IOException, HttpMessageNotWritableException { |
||||
|
||||
MediaType contentType = outputMessage.getHeaders().getContentType(); |
||||
JsonEncoding encoding = getJsonEncoding(contentType); |
||||
|
||||
Class<?> clazz = object.getClass(); |
||||
ObjectMapper objectMapper = selectObjectMapper(clazz, contentType); |
||||
Assert.state(objectMapper != null, () -> "No ObjectMapper for " + clazz.getName()); |
||||
|
||||
OutputStream outputStream = StreamUtils.nonClosing(outputMessage.getBody()); |
||||
Class<?> jsonView = null; |
||||
FilterProvider filters = null; |
||||
JavaType javaType = null; |
||||
|
||||
Type type = resolvableType.getType(); |
||||
if (TypeUtils.isAssignable(type, object.getClass())) { |
||||
javaType = getJavaType(type, null); |
||||
} |
||||
if (hints != null) { |
||||
jsonView = (Class<?>) hints.get(JSON_VIEW_HINT); |
||||
filters = (FilterProvider) hints.get(FILTER_PROVIDER_HINT); |
||||
} |
||||
|
||||
ObjectWriter objectWriter = (jsonView != null ? |
||||
objectMapper.writerWithView(jsonView) : objectMapper.writer()); |
||||
if (filters != null) { |
||||
objectWriter = objectWriter.with(filters); |
||||
} |
||||
if (javaType != null && (javaType.isContainerType() || javaType.isTypeOrSubTypeOf(Optional.class))) { |
||||
objectWriter = objectWriter.forType(javaType); |
||||
} |
||||
SerializationConfig config = objectWriter.getConfig(); |
||||
if (contentType != null && contentType.isCompatibleWith(MediaType.TEXT_EVENT_STREAM) && |
||||
config.isEnabled(SerializationFeature.INDENT_OUTPUT)) { |
||||
objectWriter = objectWriter.with(this.ssePrettyPrinter); |
||||
} |
||||
objectWriter = customizeWriter(objectWriter, javaType, contentType); |
||||
|
||||
try (JsonGenerator generator = objectWriter.createGenerator(outputStream, encoding)) { |
||||
writePrefix(generator, object); |
||||
objectWriter.writeValue(generator, object); |
||||
writeSuffix(generator, object); |
||||
generator.flush(); |
||||
} |
||||
catch (InvalidDefinitionException ex) { |
||||
throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex); |
||||
} |
||||
catch (JacksonException ex) { |
||||
throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getOriginalMessage(), ex); |
||||
} |
||||
} |
||||
|
||||
/** |
||||
* Subclasses can use this method to customize {@link ObjectWriter} used |
||||
* for writing values. |
||||
* @param writer the writer instance to customize |
||||
* @param javaType the type of element values to write |
||||
* @param contentType the selected media type |
||||
* @return the customized {@link ObjectWriter} |
||||
*/ |
||||
protected ObjectWriter customizeWriter( |
||||
ObjectWriter writer, @Nullable JavaType javaType, @Nullable MediaType contentType) { |
||||
|
||||
return writer; |
||||
} |
||||
|
||||
/** |
||||
* Write a prefix before the main content. |
||||
* @param generator the generator to use for writing content. |
||||
* @param object the object to write to the output message. |
||||
*/ |
||||
protected void writePrefix(JsonGenerator generator, Object object) { |
||||
} |
||||
|
||||
/** |
||||
* Write a suffix after the main content. |
||||
* @param generator the generator to use for writing content. |
||||
* @param object the object to write to the output message. |
||||
*/ |
||||
protected void writeSuffix(JsonGenerator generator, Object object) { |
||||
} |
||||
|
||||
/** |
||||
* Return the Jackson {@link JavaType} for the specified type and context class. |
||||
* @param type the generic type to return the Jackson JavaType for |
||||
* @param contextClass a context class for the target type, for example a class
|
||||
* in which the target type appears in a method signature (can be {@code null}) |
||||
* @return the Jackson JavaType |
||||
*/ |
||||
protected JavaType getJavaType(Type type, @Nullable Class<?> contextClass) { |
||||
return this.defaultObjectMapper.constructType(GenericTypeResolver.resolveType(type, contextClass)); |
||||
} |
||||
|
||||
/** |
||||
* Determine the JSON encoding to use for the given content type. |
||||
* @param contentType the media type as requested by the caller |
||||
* @return the JSON encoding to use (never {@code null}) |
||||
*/ |
||||
protected JsonEncoding getJsonEncoding(@Nullable MediaType contentType) { |
||||
if (contentType != null && contentType.getCharset() != null) { |
||||
Charset charset = contentType.getCharset(); |
||||
JsonEncoding encoding = ENCODINGS.get(charset.name()); |
||||
if (encoding != null) { |
||||
return encoding; |
||||
} |
||||
} |
||||
return JsonEncoding.UTF8; |
||||
} |
||||
|
||||
@Override |
||||
protected boolean supportsRepeatableWrites(Object o) { |
||||
return true; |
||||
} |
||||
} |
||||
@ -0,0 +1,61 @@
@@ -0,0 +1,61 @@
|
||||
/* |
||||
* 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.converter.cbor; |
||||
|
||||
import tools.jackson.databind.cfg.MapperBuilder; |
||||
import tools.jackson.dataformat.cbor.CBORMapper; |
||||
|
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.http.converter.AbstractJacksonHttpMessageConverter; |
||||
|
||||
/** |
||||
* Implementation of {@link org.springframework.http.converter.HttpMessageConverter |
||||
* HttpMessageConverter} that can read and write the <a href="https://cbor.io/">CBOR</a> |
||||
* data format using <a href="https://github.com/FasterXML/jackson-dataformats-binary/tree/3.x/cbor"> |
||||
* the dedicated Jackson 3.x extension</a>. |
||||
* |
||||
* <p>By default, this converter supports the {@link MediaType#APPLICATION_CBOR_VALUE} |
||||
* media type. This can be overridden by setting the {@link #setSupportedMediaTypes |
||||
* supportedMediaTypes} property. |
||||
* |
||||
* <p>The default constructor loads {@link tools.jackson.databind.JacksonModule}s |
||||
* found by {@link MapperBuilder#findModules(ClassLoader)}. |
||||
* |
||||
* @author Sebastien Deleuze |
||||
* @since 7.0 |
||||
*/ |
||||
public class JacksonCborHttpMessageConverter extends AbstractJacksonHttpMessageConverter { |
||||
|
||||
/** |
||||
* Construct a new instance with a {@link CBORMapper} customized with the |
||||
* {@link tools.jackson.databind.JacksonModule}s found by |
||||
* {@link MapperBuilder#findModules(ClassLoader)}. |
||||
*/ |
||||
public JacksonCborHttpMessageConverter() { |
||||
super(CBORMapper.builder(), MediaType.APPLICATION_CBOR); |
||||
} |
||||
|
||||
/** |
||||
* Construct a new instance with the provided {@link CBORMapper}. |
||||
* @see CBORMapper#builder() |
||||
* @see MapperBuilder#findAndAddModules(ClassLoader) |
||||
*/ |
||||
public JacksonCborHttpMessageConverter(CBORMapper mapper) { |
||||
super(mapper, MediaType.APPLICATION_CBOR); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,124 @@
@@ -0,0 +1,124 @@
|
||||
/* |
||||
* 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.converter.json; |
||||
|
||||
import java.util.Collections; |
||||
import java.util.List; |
||||
|
||||
import org.jspecify.annotations.Nullable; |
||||
import tools.jackson.core.JsonGenerator; |
||||
import tools.jackson.databind.ObjectMapper; |
||||
import tools.jackson.databind.cfg.MapperBuilder; |
||||
import tools.jackson.databind.json.JsonMapper; |
||||
|
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.http.ProblemDetail; |
||||
import org.springframework.http.converter.AbstractJacksonHttpMessageConverter; |
||||
|
||||
/** |
||||
* Implementation of {@link org.springframework.http.converter.HttpMessageConverter} |
||||
* that can read and write JSON using <a href="https://github.com/FasterXML/jackson">Jackson 3.x's</a> |
||||
* {@link ObjectMapper}. |
||||
* |
||||
* <p>This converter can be used to bind to typed beans, or untyped |
||||
* {@code HashMap} instances. |
||||
* |
||||
* <p>By default, this converter supports {@code application/json} and |
||||
* {@code application/*+json} with {@code UTF-8} character set. This |
||||
* can be overridden by setting the {@link #setSupportedMediaTypes supportedMediaTypes} |
||||
* property. |
||||
* |
||||
* <p>The default constructor loads {@link tools.jackson.databind.JacksonModule}s |
||||
* found by {@link MapperBuilder#findModules(ClassLoader)}. |
||||
* |
||||
* <p>The following hints entries are supported: |
||||
* <ul> |
||||
* <li>A JSON view with a <code>com.fasterxml.jackson.annotation.JsonView</code> |
||||
* key and the class name of the JSON view as value.</li> |
||||
* <li>A filter provider with a <code>tools.jackson.databind.ser.FilterProvider</code> |
||||
* key and the filter provider class name as value.</li> |
||||
* </ul> |
||||
* |
||||
* @author Sebastien Deleuze |
||||
* @since 7.0 |
||||
*/ |
||||
public class JacksonJsonHttpMessageConverter extends AbstractJacksonHttpMessageConverter { |
||||
|
||||
private static final List<MediaType> problemDetailMediaTypes = |
||||
Collections.singletonList(MediaType.APPLICATION_PROBLEM_JSON); |
||||
|
||||
private static final MediaType[] DEFAULT_JSON_MIME_TYPES = new MediaType[] { |
||||
MediaType.APPLICATION_JSON, new MediaType("application", "*+json") }; |
||||
|
||||
|
||||
private @Nullable String jsonPrefix; |
||||
|
||||
|
||||
/** |
||||
* 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 JacksonJsonHttpMessageConverter() { |
||||
super(JsonMapper.builder().addMixIn(ProblemDetail.class, ProblemDetailJacksonMixin.class), DEFAULT_JSON_MIME_TYPES); |
||||
} |
||||
|
||||
/** |
||||
* Construct a new instance with the provided {@link ObjectMapper}. |
||||
* @see JsonMapper#builder() |
||||
* @see MapperBuilder#findModules(ClassLoader) |
||||
*/ |
||||
public JacksonJsonHttpMessageConverter(ObjectMapper objectMapper) { |
||||
super(objectMapper, DEFAULT_JSON_MIME_TYPES); |
||||
} |
||||
|
||||
|
||||
/** |
||||
* Specify a custom prefix to use for this view's JSON output. |
||||
* Default is none. |
||||
* @see #setPrefixJson |
||||
*/ |
||||
public void setJsonPrefix(String jsonPrefix) { |
||||
this.jsonPrefix = jsonPrefix; |
||||
} |
||||
|
||||
/** |
||||
* Indicate whether the JSON output by this view should be prefixed with ")]}', ". Default is {@code false}. |
||||
* <p>Prefixing the JSON string in this manner is used to help prevent JSON Hijacking. |
||||
* The prefix renders the string syntactically invalid as a script so that it cannot be hijacked. |
||||
* This prefix should be stripped before parsing the string as JSON. |
||||
* @see #setJsonPrefix |
||||
*/ |
||||
public void setPrefixJson(boolean prefixJson) { |
||||
this.jsonPrefix = (prefixJson ? ")]}', " : null); |
||||
} |
||||
|
||||
|
||||
@Override |
||||
protected List<MediaType> getMediaTypesForProblemDetail() { |
||||
return problemDetailMediaTypes; |
||||
} |
||||
|
||||
@Override |
||||
protected void writePrefix(JsonGenerator generator, Object object) { |
||||
if (this.jsonPrefix != null) { |
||||
generator.writeRaw(this.jsonPrefix); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,63 @@
@@ -0,0 +1,63 @@
|
||||
/* |
||||
* 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.converter.smile; |
||||
|
||||
import tools.jackson.databind.cfg.MapperBuilder; |
||||
import tools.jackson.dataformat.smile.SmileMapper; |
||||
|
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.http.converter.AbstractJacksonHttpMessageConverter; |
||||
|
||||
/** |
||||
* Implementation of {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverter} |
||||
* that can read and write Smile data format ("binary JSON") using |
||||
* <a href="https://github.com/FasterXML/jackson-dataformats-binary/tree/3.x/smile"> |
||||
* the dedicated Jackson 3.x extension</a>. |
||||
* |
||||
* <p>By default, this converter supports {@code "application/x-jackson-smile"} |
||||
* media type. This can be overridden by setting the |
||||
* {@link #setSupportedMediaTypes supportedMediaTypes} property. |
||||
* |
||||
* <p>The default constructor loads {@link tools.jackson.databind.JacksonModule}s |
||||
* found by {@link MapperBuilder#findModules(ClassLoader)}. |
||||
* |
||||
* @author Sebastien Deleuze |
||||
* @since 7.0 |
||||
*/ |
||||
public class JacksonSmileHttpMessageConverter extends AbstractJacksonHttpMessageConverter { |
||||
|
||||
private static final MediaType DEFAULT_SMILE_MIME_TYPES = new MediaType("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 JacksonSmileHttpMessageConverter() { |
||||
super(SmileMapper.builder(), DEFAULT_SMILE_MIME_TYPES); |
||||
} |
||||
|
||||
/** |
||||
* Construct a new instance with the provided {@link SmileMapper}. |
||||
* @see SmileMapper#builder() |
||||
* @see MapperBuilder#findAndAddModules(ClassLoader) |
||||
*/ |
||||
public JacksonSmileHttpMessageConverter(SmileMapper mapper) { |
||||
super(mapper, DEFAULT_SMILE_MIME_TYPES); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,111 @@
@@ -0,0 +1,111 @@
|
||||
/* |
||||
* 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.converter.xml; |
||||
|
||||
import java.nio.charset.StandardCharsets; |
||||
import java.util.Collections; |
||||
import java.util.List; |
||||
|
||||
import tools.jackson.databind.cfg.MapperBuilder; |
||||
import tools.jackson.dataformat.xml.XmlFactory; |
||||
import tools.jackson.dataformat.xml.XmlMapper; |
||||
|
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.http.ProblemDetail; |
||||
import org.springframework.http.converter.AbstractJacksonHttpMessageConverter; |
||||
import org.springframework.http.converter.json.ProblemDetailJacksonXmlMixin; |
||||
import org.springframework.util.xml.StaxUtils; |
||||
|
||||
/** |
||||
* Implementation of {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverter} |
||||
* that can read and write XML using <a href="https://github.com/FasterXML/jackson-dataformat-xml"> |
||||
* Jackson 3.x extension component for reading and writing XML encoded data</a>. |
||||
* |
||||
* <p>By default, this converter supports {@code application/xml}, {@code text/xml}, and |
||||
* {@code application/*+xml} with {@code UTF-8} character set. This can be overridden by |
||||
* setting the {@link #setSupportedMediaTypes supportedMediaTypes} property. |
||||
* |
||||
* <p>The default constructor loads {@link tools.jackson.databind.JacksonModule}s |
||||
* found by {@link MapperBuilder#findModules(ClassLoader)}. |
||||
* |
||||
* <p>The following hint entries are supported: |
||||
* <ul> |
||||
* <li>A JSON view with a <code>com.fasterxml.jackson.annotation.JsonView</code> |
||||
* key and the class name of the JSON view as value.</li> |
||||
* <li>A filter provider with a <code>tools.jackson.databind.ser.FilterProvider</code> |
||||
* key and the filter provider class name as value.</li> |
||||
* </ul> |
||||
* |
||||
* @author Sebastien Deleuze |
||||
* @since 7.0 |
||||
*/ |
||||
public class JacksonXmlHttpMessageConverter extends AbstractJacksonHttpMessageConverter { |
||||
|
||||
private static final List<MediaType> problemDetailMediaTypes = |
||||
Collections.singletonList(MediaType.APPLICATION_PROBLEM_XML); |
||||
|
||||
private static final MediaType[] DEFAULT_XML_MIME_TYPES = new MediaType[] { |
||||
new MediaType("application", "xml", StandardCharsets.UTF_8), |
||||
new MediaType("text", "xml", StandardCharsets.UTF_8), |
||||
new MediaType("application", "*+xml", StandardCharsets.UTF_8) |
||||
}; |
||||
|
||||
/** |
||||
* Construct a new instance with a {@link XmlMapper} created from |
||||
* {@link #defensiveXmlFactory} and customized with the |
||||
* {@link tools.jackson.databind.JacksonModule}s found by |
||||
* {@link MapperBuilder#findModules(ClassLoader)} and |
||||
* {@link ProblemDetailJacksonXmlMixin}. |
||||
*/ |
||||
public JacksonXmlHttpMessageConverter() { |
||||
this(XmlMapper.builder(defensiveXmlFactory())); |
||||
} |
||||
|
||||
/** |
||||
* Construct a new instance with the provided {@link XmlMapper.Builder builder} |
||||
* customized with the {@link tools.jackson.databind.JacksonModule}s found by |
||||
* {@link MapperBuilder#findModules(ClassLoader)} and |
||||
* {@link ProblemDetailJacksonXmlMixin}. |
||||
*/ |
||||
public JacksonXmlHttpMessageConverter(XmlMapper.Builder builder) { |
||||
super(builder.addMixIn(ProblemDetail.class, ProblemDetailJacksonXmlMixin.class), DEFAULT_XML_MIME_TYPES); |
||||
} |
||||
|
||||
/** |
||||
* Construct a new instance with the provided {@link XmlMapper}. |
||||
* @see XmlMapper#builder() |
||||
* @see MapperBuilder#findModules(ClassLoader) |
||||
*/ |
||||
public JacksonXmlHttpMessageConverter(XmlMapper xmlMapper) { |
||||
super(xmlMapper, DEFAULT_XML_MIME_TYPES); |
||||
} |
||||
|
||||
/** |
||||
* Return an {@link XmlFactory} created from {@link StaxUtils#createDefensiveInputFactory} |
||||
* with Spring's defensive setup, i.e. no support for the resolution of DTDs and external |
||||
* entities. |
||||
*/ |
||||
public static XmlFactory defensiveXmlFactory() { |
||||
return new XmlFactory(StaxUtils.createDefensiveInputFactory()); |
||||
} |
||||
|
||||
@Override |
||||
protected List<MediaType> getMediaTypesForProblemDetail() { |
||||
return problemDetailMediaTypes; |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,61 @@
@@ -0,0 +1,61 @@
|
||||
/* |
||||
* 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.converter.yaml; |
||||
|
||||
import tools.jackson.databind.cfg.MapperBuilder; |
||||
import tools.jackson.dataformat.yaml.YAMLMapper; |
||||
|
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.http.converter.AbstractJacksonHttpMessageConverter; |
||||
|
||||
/** |
||||
* Implementation of {@link org.springframework.http.converter.HttpMessageConverter |
||||
* HttpMessageConverter} that can read and write the <a href="https://yaml.io/">YAML</a> |
||||
* data format using <a href="https://github.com/FasterXML/jackson-dataformats-text/tree/3.x/yaml"> |
||||
* the dedicated Jackson 3.x extension</a>. |
||||
* |
||||
* <p>By default, this converter supports the {@link MediaType#APPLICATION_YAML_VALUE} |
||||
* media type. This can be overridden by setting the {@link #setSupportedMediaTypes |
||||
* supportedMediaTypes} property. |
||||
* |
||||
* <p>The default constructor loads {@link tools.jackson.databind.JacksonModule}s |
||||
* found by {@link MapperBuilder#findModules(ClassLoader)}. |
||||
* |
||||
* @author Sebastien Deleuze |
||||
* @since 7.0 |
||||
*/ |
||||
public class JacksonYamlHttpMessageConverter extends AbstractJacksonHttpMessageConverter { |
||||
|
||||
/** |
||||
* Construct a new instance with a {@link YAMLMapper} customized with the |
||||
* {@link tools.jackson.databind.JacksonModule}s found by |
||||
* {@link MapperBuilder#findModules(ClassLoader)}. |
||||
*/ |
||||
public JacksonYamlHttpMessageConverter() { |
||||
super(YAMLMapper.builder(), MediaType.APPLICATION_YAML); |
||||
} |
||||
|
||||
/** |
||||
* Construct a new instance with the provided {@link YAMLMapper}. |
||||
* @see YAMLMapper#builder() |
||||
* @see MapperBuilder#findAndAddModules(ClassLoader) |
||||
*/ |
||||
public JacksonYamlHttpMessageConverter(YAMLMapper mapper) { |
||||
super(mapper, MediaType.APPLICATION_YAML); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,157 @@
@@ -0,0 +1,157 @@
|
||||
/* |
||||
* 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.converter.cbor; |
||||
|
||||
import java.io.IOException; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
import tools.jackson.dataformat.cbor.CBORMapper; |
||||
|
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.web.testfixture.http.MockHttpInputMessage; |
||||
import org.springframework.web.testfixture.http.MockHttpOutputMessage; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.within; |
||||
|
||||
/** |
||||
* Jackson 3.x CBOR converter tests. |
||||
* |
||||
* @author Sebastien Deleuze |
||||
*/ |
||||
class JacksonCborHttpMessageConverterTests { |
||||
|
||||
private final JacksonCborHttpMessageConverter converter = new JacksonCborHttpMessageConverter(); |
||||
private final CBORMapper mapper = CBORMapper.builder().build(); |
||||
|
||||
|
||||
@Test |
||||
void canRead() { |
||||
assertThat(converter.canRead(MyBean.class, MediaType.APPLICATION_CBOR)).isTrue(); |
||||
assertThat(converter.canRead(MyBean.class, new MediaType("application", "json"))).isFalse(); |
||||
assertThat(converter.canRead(MyBean.class, new MediaType("application", "xml"))).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
void canWrite() { |
||||
assertThat(converter.canWrite(MyBean.class, MediaType.APPLICATION_CBOR)).isTrue(); |
||||
assertThat(converter.canWrite(MyBean.class, new MediaType("application", "json"))).isFalse(); |
||||
assertThat(converter.canWrite(MyBean.class, new MediaType("application", "xml"))).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
void read() throws IOException { |
||||
MyBean body = new MyBean(); |
||||
body.setString("Foo"); |
||||
body.setNumber(42); |
||||
body.setFraction(42F); |
||||
body.setArray(new String[]{"Foo", "Bar"}); |
||||
body.setBool(true); |
||||
body.setBytes(new byte[]{0x1, 0x2}); |
||||
MockHttpInputMessage inputMessage = new MockHttpInputMessage(mapper.writeValueAsBytes(body)); |
||||
inputMessage.getHeaders().setContentType(MediaType.APPLICATION_CBOR); |
||||
MyBean result = (MyBean) converter.read(MyBean.class, inputMessage); |
||||
assertThat(result.getString()).isEqualTo("Foo"); |
||||
assertThat(result.getNumber()).isEqualTo(42); |
||||
assertThat(result.getFraction()).isCloseTo(42F, within(0F)); |
||||
|
||||
assertThat(result.getArray()).isEqualTo(new String[]{"Foo", "Bar"}); |
||||
assertThat(result.isBool()).isTrue(); |
||||
assertThat(result.getBytes()).isEqualTo(new byte[]{0x1, 0x2}); |
||||
} |
||||
|
||||
@Test |
||||
void write() throws IOException { |
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); |
||||
MyBean body = new MyBean(); |
||||
body.setString("Foo"); |
||||
body.setNumber(42); |
||||
body.setFraction(42F); |
||||
body.setArray(new String[]{"Foo", "Bar"}); |
||||
body.setBool(true); |
||||
body.setBytes(new byte[]{0x1, 0x2}); |
||||
converter.write(body, null, outputMessage); |
||||
assertThat(outputMessage.getBodyAsBytes()).isEqualTo(mapper.writeValueAsBytes(body)); |
||||
assertThat(outputMessage.getHeaders().getContentType()) |
||||
.as("Invalid content-type").isEqualTo(MediaType.APPLICATION_CBOR); |
||||
} |
||||
|
||||
|
||||
public static class MyBean { |
||||
|
||||
private String string; |
||||
|
||||
private int number; |
||||
|
||||
private float fraction; |
||||
|
||||
private String[] array; |
||||
|
||||
private boolean bool; |
||||
|
||||
private byte[] bytes; |
||||
|
||||
public byte[] getBytes() { |
||||
return bytes; |
||||
} |
||||
|
||||
public void setBytes(byte[] bytes) { |
||||
this.bytes = bytes; |
||||
} |
||||
|
||||
public boolean isBool() { |
||||
return bool; |
||||
} |
||||
|
||||
public void setBool(boolean bool) { |
||||
this.bool = bool; |
||||
} |
||||
|
||||
public String getString() { |
||||
return string; |
||||
} |
||||
|
||||
public void setString(String string) { |
||||
this.string = string; |
||||
} |
||||
|
||||
public int getNumber() { |
||||
return number; |
||||
} |
||||
|
||||
public void setNumber(int number) { |
||||
this.number = number; |
||||
} |
||||
|
||||
public float getFraction() { |
||||
return fraction; |
||||
} |
||||
|
||||
public void setFraction(float fraction) { |
||||
this.fraction = fraction; |
||||
} |
||||
|
||||
public String[] getArray() { |
||||
return array; |
||||
} |
||||
|
||||
public void setArray(String[] array) { |
||||
this.array = array; |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,843 @@
@@ -0,0 +1,843 @@
|
||||
/* |
||||
* 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.converter.json; |
||||
|
||||
import java.io.IOException; |
||||
import java.lang.reflect.Type; |
||||
import java.nio.charset.Charset; |
||||
import java.nio.charset.StandardCharsets; |
||||
import java.util.ArrayList; |
||||
import java.util.Collections; |
||||
import java.util.HashMap; |
||||
import java.util.List; |
||||
import java.util.Map; |
||||
import java.util.Optional; |
||||
|
||||
import com.fasterxml.jackson.annotation.JsonFilter; |
||||
import com.fasterxml.jackson.annotation.JsonSubTypes; |
||||
import com.fasterxml.jackson.annotation.JsonTypeInfo; |
||||
import com.fasterxml.jackson.annotation.JsonView; |
||||
import org.jspecify.annotations.Nullable; |
||||
import org.junit.jupiter.api.Test; |
||||
import org.skyscreamer.jsonassert.JSONAssert; |
||||
import tools.jackson.databind.JavaType; |
||||
import tools.jackson.databind.ObjectMapper; |
||||
import tools.jackson.databind.ObjectReader; |
||||
import tools.jackson.databind.ObjectWriter; |
||||
import tools.jackson.databind.SerializationFeature; |
||||
import tools.jackson.databind.cfg.EnumFeature; |
||||
import tools.jackson.databind.json.JsonMapper; |
||||
import tools.jackson.databind.ser.FilterProvider; |
||||
import tools.jackson.databind.ser.std.SimpleBeanPropertyFilter; |
||||
import tools.jackson.databind.ser.std.SimpleFilterProvider; |
||||
|
||||
import org.springframework.core.ParameterizedTypeReference; |
||||
import org.springframework.core.ResolvableType; |
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.http.converter.HttpMessageNotReadableException; |
||||
import org.springframework.web.testfixture.http.MockHttpInputMessage; |
||||
import org.springframework.web.testfixture.http.MockHttpOutputMessage; |
||||
|
||||
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.assertj.core.api.Assertions.entry; |
||||
import static org.assertj.core.api.Assertions.within; |
||||
|
||||
/** |
||||
* Jackson 3.x converter tests. |
||||
* |
||||
* @author Sebastien Deleuze |
||||
*/ |
||||
class JacksonJsonHttpMessageConverterTests { |
||||
|
||||
private JacksonJsonHttpMessageConverter converter = new JacksonJsonHttpMessageConverter(); |
||||
|
||||
|
||||
@Test |
||||
void canRead() { |
||||
assertThat(converter.canRead(MyBean.class, new MediaType("application", "json"))).isTrue(); |
||||
assertThat(converter.canRead(Map.class, new MediaType("application", "json"))).isTrue(); |
||||
assertThat(converter.canRead(MyBean.class, new MediaType("application", "json", StandardCharsets.UTF_8))).isTrue(); |
||||
assertThat(converter.canRead(MyBean.class, new MediaType("application", "json", StandardCharsets.US_ASCII))).isTrue(); |
||||
assertThat(converter.canRead(MyBean.class, new MediaType("application", "json", StandardCharsets.ISO_8859_1))).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
void canReadWithObjectMapperRegistrationForType() { |
||||
MediaType halJsonMediaType = MediaType.parseMediaType("application/hal+json"); |
||||
MediaType halFormsJsonMediaType = MediaType.parseMediaType("application/prs.hal-forms+json"); |
||||
|
||||
assertThat(converter.canRead(MyBean.class, halJsonMediaType)).isTrue(); |
||||
assertThat(converter.canRead(MyBean.class, MediaType.APPLICATION_JSON)).isTrue(); |
||||
assertThat(converter.canRead(MyBean.class, halFormsJsonMediaType)).isTrue(); |
||||
assertThat(converter.canRead(Map.class, MediaType.APPLICATION_JSON)).isTrue(); |
||||
|
||||
converter.registerObjectMappersForType(MyBean.class, map -> { |
||||
map.put(halJsonMediaType, new ObjectMapper()); |
||||
map.put(MediaType.APPLICATION_JSON, new ObjectMapper()); |
||||
}); |
||||
|
||||
assertThat(converter.canRead(MyBean.class, halJsonMediaType)).isTrue(); |
||||
assertThat(converter.canRead(MyBean.class, MediaType.APPLICATION_JSON)).isTrue(); |
||||
assertThat(converter.canRead(Map.class, MediaType.APPLICATION_JSON)).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
void canWrite() { |
||||
assertThat(converter.canWrite(MyBean.class, MediaType.APPLICATION_JSON)).isTrue(); |
||||
assertThat(converter.canWrite(Map.class, MediaType.APPLICATION_JSON)).isTrue(); |
||||
assertThat(converter.canWrite(MyBean.class, new MediaType("application", "json", StandardCharsets.UTF_8))).isTrue(); |
||||
assertThat(converter.canWrite(MyBean.class, new MediaType("application", "json", StandardCharsets.US_ASCII))).isTrue(); |
||||
assertThat(converter.canWrite(MyBean.class, new MediaType("application", "json", StandardCharsets.ISO_8859_1))).isFalse(); |
||||
assertThatThrownBy(() -> converter.canWrite(MappingJacksonValue.class, MediaType.APPLICATION_JSON)).isInstanceOf(UnsupportedOperationException.class); |
||||
} |
||||
|
||||
@Test // SPR-7905
|
||||
void canReadAndWriteMicroformats() { |
||||
assertThat(converter.canRead(MyBean.class, new MediaType("application", "vnd.test-micro-type+json"))).isTrue(); |
||||
assertThat(converter.canWrite(MyBean.class, new MediaType("application", "vnd.test-micro-type+json"))).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
void getSupportedMediaTypes() { |
||||
MediaType[] defaultMediaTypes = {MediaType.APPLICATION_JSON, MediaType.parseMediaType("application/*+json")}; |
||||
assertThat(converter.getSupportedMediaTypes()).containsExactly(defaultMediaTypes); |
||||
assertThat(converter.getSupportedMediaTypes(MyBean.class)).containsExactly(defaultMediaTypes); |
||||
|
||||
MediaType halJson = MediaType.parseMediaType("application/hal+json"); |
||||
converter.registerObjectMappersForType(MyBean.class, map -> { |
||||
map.put(halJson, new ObjectMapper()); |
||||
map.put(MediaType.APPLICATION_JSON, new ObjectMapper()); |
||||
}); |
||||
|
||||
assertThat(converter.getSupportedMediaTypes(MyBean.class)).containsExactly(halJson, MediaType.APPLICATION_JSON); |
||||
assertThat(converter.getSupportedMediaTypes(Map.class)).containsExactly(defaultMediaTypes); |
||||
} |
||||
|
||||
@Test |
||||
void readTyped() throws IOException { |
||||
String body = "{" + |
||||
"\"bytes\":\"AQI=\"," + |
||||
"\"array\":[\"Foo\",\"Bar\"]," + |
||||
"\"number\":42," + |
||||
"\"string\":\"Foo\"," + |
||||
"\"bool\":true," + |
||||
"\"fraction\":42.0}"; |
||||
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); |
||||
inputMessage.getHeaders().setContentType(new MediaType("application", "json")); |
||||
MyBean result = (MyBean) converter.read(MyBean.class, inputMessage); |
||||
assertThat(result.getString()).isEqualTo("Foo"); |
||||
assertThat(result.getNumber()).isEqualTo(42); |
||||
assertThat(result.getFraction()).isCloseTo(42F, within(0F)); |
||||
assertThat(result.getArray()).isEqualTo(new String[] {"Foo", "Bar"}); |
||||
assertThat(result.isBool()).isTrue(); |
||||
assertThat(result.getBytes()).isEqualTo(new byte[] {0x1, 0x2}); |
||||
} |
||||
|
||||
@Test |
||||
@SuppressWarnings("unchecked") |
||||
void readUntyped() throws IOException { |
||||
String body = "{" + |
||||
"\"bytes\":\"AQI=\"," + |
||||
"\"array\":[\"Foo\",\"Bar\"]," + |
||||
"\"number\":42," + |
||||
"\"string\":\"Foo\"," + |
||||
"\"bool\":true," + |
||||
"\"fraction\":42.0}"; |
||||
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); |
||||
inputMessage.getHeaders().setContentType(new MediaType("application", "json")); |
||||
HashMap<String, Object> result = (HashMap<String, Object>) converter.read(HashMap.class, inputMessage); |
||||
assertThat(result).containsEntry("string", "Foo"); |
||||
assertThat(result).containsEntry("number", 42); |
||||
assertThat((Double) result.get("fraction")).isCloseTo(42D, within(0D)); |
||||
List<String> array = new ArrayList<>(); |
||||
array.add("Foo"); |
||||
array.add("Bar"); |
||||
assertThat(result).containsEntry("array", array); |
||||
assertThat(result).containsEntry("bool", Boolean.TRUE); |
||||
assertThat(result).containsEntry("bytes", "AQI="); |
||||
} |
||||
|
||||
@Test |
||||
void write() throws IOException { |
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); |
||||
MyBean body = new MyBean(); |
||||
body.setString("Foo"); |
||||
body.setNumber(42); |
||||
body.setFraction(42F); |
||||
body.setArray(new String[] {"Foo", "Bar"}); |
||||
body.setBool(true); |
||||
body.setBytes(new byte[] {0x1, 0x2}); |
||||
converter.write(body, null, outputMessage); |
||||
String result = outputMessage.getBodyAsString(StandardCharsets.UTF_8); |
||||
assertThat(result).contains("\"string\":\"Foo\""); |
||||
assertThat(result).contains("\"number\":42"); |
||||
assertThat(result).contains("fraction\":42.0"); |
||||
assertThat(result).contains("\"array\":[\"Foo\",\"Bar\"]"); |
||||
assertThat(result).contains("\"bool\":true"); |
||||
assertThat(result).contains("\"bytes\":\"AQI=\""); |
||||
assertThat(outputMessage.getHeaders().getContentType()) |
||||
.as("Invalid content-type").isEqualTo(MediaType.APPLICATION_JSON); |
||||
} |
||||
|
||||
@Test |
||||
void writeWithBaseType() throws IOException { |
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); |
||||
MyBean body = new MyBean(); |
||||
body.setString("Foo"); |
||||
body.setNumber(42); |
||||
body.setFraction(42F); |
||||
body.setArray(new String[] {"Foo", "Bar"}); |
||||
body.setBool(true); |
||||
body.setBytes(new byte[] {0x1, 0x2}); |
||||
converter.write(body, ResolvableType.forClass(MyBase.class), null, outputMessage, null); |
||||
String result = outputMessage.getBodyAsString(StandardCharsets.UTF_8); |
||||
assertThat(result).contains("\"string\":\"Foo\""); |
||||
assertThat(result).contains("\"number\":42"); |
||||
assertThat(result).contains("fraction\":42.0"); |
||||
assertThat(result).contains("\"array\":[\"Foo\",\"Bar\"]"); |
||||
assertThat(result).contains("\"bool\":true"); |
||||
assertThat(result).contains("\"bytes\":\"AQI=\""); |
||||
assertThat(outputMessage.getHeaders().getContentType()) |
||||
.as("Invalid content-type").isEqualTo(MediaType.APPLICATION_JSON); |
||||
} |
||||
|
||||
@Test |
||||
void writeUTF16() throws IOException { |
||||
MediaType contentType = new MediaType("application", "json", StandardCharsets.UTF_16BE); |
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); |
||||
String body = "H\u00e9llo W\u00f6rld"; |
||||
converter.write(body, contentType, outputMessage); |
||||
assertThat(outputMessage.getBodyAsString(StandardCharsets.UTF_16BE)).as("Invalid result").isEqualTo(("\"" + body + "\"")); |
||||
assertThat(outputMessage.getHeaders().getContentType()).as("Invalid content-type").isEqualTo(contentType); |
||||
} |
||||
|
||||
@Test |
||||
void readInvalidJson() { |
||||
String body = "FooBar"; |
||||
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); |
||||
inputMessage.getHeaders().setContentType(new MediaType("application", "json")); |
||||
assertThatExceptionOfType(HttpMessageNotReadableException.class) |
||||
.isThrownBy(() -> converter.read(MyBean.class, inputMessage)); |
||||
} |
||||
|
||||
@Test |
||||
void readValidJsonWithUnknownProperty() throws IOException { |
||||
String body = "{\"string\":\"string\",\"unknownProperty\":\"value\"}"; |
||||
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); |
||||
inputMessage.getHeaders().setContentType(new MediaType("application", "json")); |
||||
converter.read(MyBean.class, inputMessage); |
||||
// Assert no HttpMessageNotReadableException is thrown
|
||||
} |
||||
|
||||
@Test |
||||
@SuppressWarnings("unchecked") |
||||
void readAndWriteGenerics() throws Exception { |
||||
JacksonJsonHttpMessageConverter converter = new JacksonJsonHttpMessageConverter() { |
||||
@Override |
||||
protected JavaType getJavaType(Type type, @Nullable Class<?> contextClass) { |
||||
if (type instanceof Class && List.class.isAssignableFrom((Class<?>)type)) { |
||||
return new ObjectMapper().getTypeFactory().constructCollectionType(ArrayList.class, MyBean.class); |
||||
} |
||||
else { |
||||
return super.getJavaType(type, contextClass); |
||||
} |
||||
} |
||||
}; |
||||
String body = "[{" + |
||||
"\"bytes\":\"AQI=\"," + |
||||
"\"array\":[\"Foo\",\"Bar\"]," + |
||||
"\"number\":42," + |
||||
"\"string\":\"Foo\"," + |
||||
"\"bool\":true," + |
||||
"\"fraction\":42.0}]"; |
||||
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); |
||||
inputMessage.getHeaders().setContentType(new MediaType("application", "json")); |
||||
|
||||
List<MyBean> results = (List<MyBean>) converter.read(List.class, inputMessage); |
||||
assertThat(results).hasSize(1); |
||||
MyBean result = results.get(0); |
||||
assertThat(result.getString()).isEqualTo("Foo"); |
||||
assertThat(result.getNumber()).isEqualTo(42); |
||||
assertThat(result.getFraction()).isCloseTo(42F, within(0F)); |
||||
assertThat(result.getArray()).isEqualTo(new String[] {"Foo", "Bar"}); |
||||
assertThat(result.isBool()).isTrue(); |
||||
assertThat(result.getBytes()).isEqualTo(new byte[] {0x1, 0x2}); |
||||
|
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); |
||||
converter.write(results, MediaType.APPLICATION_JSON, outputMessage); |
||||
JSONAssert.assertEquals(body, outputMessage.getBodyAsString(StandardCharsets.UTF_8), true); |
||||
} |
||||
|
||||
@Test |
||||
@SuppressWarnings("unchecked") |
||||
void readAndWriteParameterizedType() throws Exception { |
||||
ParameterizedTypeReference<List<MyBean>> beansList = new ParameterizedTypeReference<>() {}; |
||||
|
||||
String body = "[{" + |
||||
"\"bytes\":\"AQI=\"," + |
||||
"\"array\":[\"Foo\",\"Bar\"]," + |
||||
"\"number\":42," + |
||||
"\"string\":\"Foo\"," + |
||||
"\"bool\":true," + |
||||
"\"fraction\":42.0}]"; |
||||
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); |
||||
inputMessage.getHeaders().setContentType(MediaType.APPLICATION_JSON); |
||||
|
||||
JacksonJsonHttpMessageConverter converter = new JacksonJsonHttpMessageConverter(); |
||||
List<MyBean> results = (List<MyBean>) converter.read(ResolvableType.forType(beansList), inputMessage, null); |
||||
assertThat(results).hasSize(1); |
||||
MyBean result = results.get(0); |
||||
assertThat(result.getString()).isEqualTo("Foo"); |
||||
assertThat(result.getNumber()).isEqualTo(42); |
||||
assertThat(result.getFraction()).isCloseTo(42F, within(0F)); |
||||
assertThat(result.getArray()).isEqualTo(new String[] {"Foo", "Bar"}); |
||||
assertThat(result.isBool()).isTrue(); |
||||
assertThat(result.getBytes()).isEqualTo(new byte[] {0x1, 0x2}); |
||||
|
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); |
||||
converter.write(results, ResolvableType.forType(beansList), MediaType.APPLICATION_JSON, outputMessage, null); |
||||
JSONAssert.assertEquals(body, outputMessage.getBodyAsString(StandardCharsets.UTF_8), true); |
||||
} |
||||
|
||||
@Test |
||||
@SuppressWarnings("unchecked") |
||||
void writeParameterizedBaseType() throws Exception { |
||||
ParameterizedTypeReference<List<MyBean>> beansList = new ParameterizedTypeReference<>() {}; |
||||
ParameterizedTypeReference<List<MyBase>> baseList = new ParameterizedTypeReference<>() {}; |
||||
|
||||
String body = "[{" + |
||||
"\"bytes\":\"AQI=\"," + |
||||
"\"array\":[\"Foo\",\"Bar\"]," + |
||||
"\"number\":42," + |
||||
"\"string\":\"Foo\"," + |
||||
"\"bool\":true," + |
||||
"\"fraction\":42.0}]"; |
||||
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); |
||||
inputMessage.getHeaders().setContentType(MediaType.APPLICATION_JSON); |
||||
|
||||
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); |
||||
List<MyBean> results = (List<MyBean>) converter.read(beansList.getType(), null, inputMessage); |
||||
assertThat(results).hasSize(1); |
||||
MyBean result = results.get(0); |
||||
assertThat(result.getString()).isEqualTo("Foo"); |
||||
assertThat(result.getNumber()).isEqualTo(42); |
||||
assertThat(result.getFraction()).isCloseTo(42F, within(0F)); |
||||
assertThat(result.getArray()).isEqualTo(new String[] {"Foo", "Bar"}); |
||||
assertThat(result.isBool()).isTrue(); |
||||
assertThat(result.getBytes()).isEqualTo(new byte[] {0x1, 0x2}); |
||||
|
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); |
||||
converter.write(results, baseList.getType(), MediaType.APPLICATION_JSON, outputMessage); |
||||
JSONAssert.assertEquals(body, outputMessage.getBodyAsString(StandardCharsets.UTF_8), true); |
||||
} |
||||
|
||||
// gh-24498
|
||||
@Test |
||||
void writeOptional() throws IOException { |
||||
ParameterizedTypeReference<Optional<MyParent>> optionalParent = new ParameterizedTypeReference<>() {}; |
||||
Optional<MyParent> result = Optional.of(new Impl1()); |
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); |
||||
converter.write(result, ResolvableType.forType(optionalParent.getType()), |
||||
MediaType.APPLICATION_JSON, outputMessage, null); |
||||
|
||||
assertThat(outputMessage.getBodyAsString(StandardCharsets.UTF_8)) |
||||
.contains("@type"); |
||||
} |
||||
|
||||
@Test |
||||
void prettyPrint() throws Exception { |
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); |
||||
PrettyPrintBean bean = new PrettyPrintBean(); |
||||
bean.setName("Jason"); |
||||
|
||||
ObjectMapper mapper = JsonMapper.builder().enable(SerializationFeature.INDENT_OUTPUT).build(); |
||||
this.converter = new JacksonJsonHttpMessageConverter(mapper); |
||||
this.converter.write(bean, ResolvableType.forType(PrettyPrintBean.class), |
||||
MediaType.APPLICATION_JSON, outputMessage, null); |
||||
String result = outputMessage.getBodyAsString(StandardCharsets.UTF_8); |
||||
|
||||
assertThat(result).isEqualToNormalizingNewlines(""" |
||||
{ |
||||
\s "name" : "Jason" |
||||
}"""); |
||||
} |
||||
|
||||
@Test |
||||
void prettyPrintWithSse() throws Exception { |
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); |
||||
outputMessage.getHeaders().setContentType(MediaType.TEXT_EVENT_STREAM); |
||||
PrettyPrintBean bean = new PrettyPrintBean(); |
||||
bean.setName("Jason"); |
||||
|
||||
ObjectMapper mapper = JsonMapper.builder().enable(SerializationFeature.INDENT_OUTPUT).build(); |
||||
this.converter = new JacksonJsonHttpMessageConverter(mapper); |
||||
this.converter.write(bean, ResolvableType.forType(PrettyPrintBean.class), |
||||
MediaType.APPLICATION_JSON, outputMessage, null); |
||||
String result = outputMessage.getBodyAsString(StandardCharsets.UTF_8); |
||||
|
||||
assertThat(result).isEqualTo("{\ndata: \"name\" : \"Jason\"\ndata:}"); |
||||
} |
||||
|
||||
@Test |
||||
void prefixJson() throws Exception { |
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); |
||||
this.converter.setPrefixJson(true); |
||||
this.converter.write("foo", ResolvableType.forType(String.class), MediaType.APPLICATION_JSON, |
||||
outputMessage, null); |
||||
|
||||
assertThat(outputMessage.getBodyAsString(StandardCharsets.UTF_8)).isEqualTo(")]}', \"foo\""); |
||||
} |
||||
|
||||
@Test |
||||
void prefixJsonCustom() throws Exception { |
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); |
||||
this.converter.setJsonPrefix(")))"); |
||||
this.converter.write("foo", ResolvableType.forType(String.class), MediaType.APPLICATION_JSON, |
||||
outputMessage, null); |
||||
|
||||
assertThat(outputMessage.getBodyAsString(StandardCharsets.UTF_8)).isEqualTo(")))\"foo\""); |
||||
} |
||||
|
||||
@Test |
||||
void fieldLevelJsonView() throws Exception { |
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); |
||||
JacksonViewBean bean = new JacksonViewBean(); |
||||
bean.setWithView1("with"); |
||||
bean.setWithView2("with"); |
||||
bean.setWithoutView("without"); |
||||
|
||||
Map<String, Object> hints = Collections.singletonMap(JsonView.class.getName(), MyJacksonView1.class); |
||||
this.converter.write(bean, ResolvableType.forType(JacksonViewBean.class), MediaType.APPLICATION_JSON, |
||||
outputMessage, hints); |
||||
|
||||
String result = outputMessage.getBodyAsString(StandardCharsets.UTF_8); |
||||
assertThat(result).contains("\"withView1\":\"with\""); |
||||
assertThat(result).doesNotContain("\"withView2\":\"with\""); |
||||
assertThat(result).doesNotContain("\"withoutView\":\"without\""); |
||||
} |
||||
|
||||
@Test |
||||
void classLevelJsonView() throws Exception { |
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); |
||||
JacksonViewBean bean = new JacksonViewBean(); |
||||
bean.setWithView1("with"); |
||||
bean.setWithView2("with"); |
||||
bean.setWithoutView("without"); |
||||
|
||||
Map<String, Object> hints = Collections.singletonMap(JsonView.class.getName(), MyJacksonView3.class); |
||||
this.converter.write(bean, ResolvableType.forType(JacksonViewBean.class), MediaType.APPLICATION_JSON, |
||||
outputMessage, hints); |
||||
|
||||
String result = outputMessage.getBodyAsString(StandardCharsets.UTF_8); |
||||
assertThat(result).doesNotContain("\"withView1\":\"with\""); |
||||
assertThat(result).doesNotContain("\"withView2\":\"with\""); |
||||
assertThat(result).contains("\"withoutView\":\"without\""); |
||||
} |
||||
|
||||
@Test |
||||
void filters() throws Exception { |
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); |
||||
JacksonFilteredBean bean = new JacksonFilteredBean(); |
||||
bean.setProperty1("value"); |
||||
bean.setProperty2("value"); |
||||
|
||||
|
||||
FilterProvider filters = new SimpleFilterProvider().addFilter("myJacksonFilter", |
||||
SimpleBeanPropertyFilter.serializeAllExcept("property2")); |
||||
Map<String, Object> hints = Collections.singletonMap(FilterProvider.class.getName(), filters); |
||||
this.converter.write(bean, ResolvableType.forType(JacksonFilteredBean.class), MediaType.APPLICATION_JSON, |
||||
outputMessage, hints); |
||||
|
||||
String result = outputMessage.getBodyAsString(StandardCharsets.UTF_8); |
||||
assertThat(result).contains("\"property1\":\"value\""); |
||||
assertThat(result).doesNotContain("\"property2\":\"value\""); |
||||
} |
||||
|
||||
@Test // SPR-13318
|
||||
void writeSubType() throws Exception { |
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); |
||||
MyBean bean = new MyBean(); |
||||
bean.setString("Foo"); |
||||
bean.setNumber(42); |
||||
|
||||
this.converter.write(bean, ResolvableType.forType(MyInterface.class), |
||||
MediaType.APPLICATION_JSON, outputMessage, null); |
||||
|
||||
String result = outputMessage.getBodyAsString(StandardCharsets.UTF_8); |
||||
assertThat(result).contains("\"string\":\"Foo\""); |
||||
assertThat(result).contains("\"number\":42"); |
||||
} |
||||
|
||||
@Test // SPR-13318
|
||||
void writeSubTypeList() throws Exception { |
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); |
||||
List<MyBean> beans = new ArrayList<>(); |
||||
MyBean foo = new MyBean(); |
||||
foo.setString("Foo"); |
||||
foo.setNumber(42); |
||||
beans.add(foo); |
||||
MyBean bar = new MyBean(); |
||||
bar.setString("Bar"); |
||||
bar.setNumber(123); |
||||
beans.add(bar); |
||||
ParameterizedTypeReference<List<MyInterface>> typeReference = |
||||
new ParameterizedTypeReference<>() {}; |
||||
|
||||
this.converter.write(beans, ResolvableType.forType(typeReference), MediaType.APPLICATION_JSON, |
||||
outputMessage, null); |
||||
|
||||
String result = outputMessage.getBodyAsString(StandardCharsets.UTF_8); |
||||
assertThat(result).contains("\"string\":\"Foo\""); |
||||
assertThat(result).contains("\"number\":42"); |
||||
assertThat(result).contains("\"string\":\"Bar\""); |
||||
assertThat(result).contains("\"number\":123"); |
||||
} |
||||
|
||||
@Test // gh-27511
|
||||
void readWithNoDefaultConstructor() throws Exception { |
||||
String body = "{\"property1\":\"foo\",\"property2\":\"bar\"}"; |
||||
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); |
||||
inputMessage.getHeaders().setContentType(MediaType.APPLICATION_JSON); |
||||
BeanWithNoDefaultConstructor bean = |
||||
(BeanWithNoDefaultConstructor)converter.read(BeanWithNoDefaultConstructor.class, inputMessage); |
||||
assertThat(bean.property1).isEqualTo("foo"); |
||||
assertThat(bean.property2).isEqualTo("bar"); |
||||
} |
||||
|
||||
@Test |
||||
@SuppressWarnings("unchecked") |
||||
void readNonUnicode() throws Exception { |
||||
String body = "{\"føø\":\"bår\"}"; |
||||
Charset charset = StandardCharsets.ISO_8859_1; |
||||
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(charset)); |
||||
inputMessage.getHeaders().setContentType(new MediaType("application", "json", charset)); |
||||
HashMap<String, Object> result = (HashMap<String, Object>) this.converter.read(HashMap.class, inputMessage); |
||||
|
||||
assertThat(result).containsExactly(entry("føø", "bår")); |
||||
} |
||||
|
||||
@Test |
||||
@SuppressWarnings("unchecked") |
||||
void readAscii() throws Exception { |
||||
String body = "{\"foo\":\"bar\"}"; |
||||
Charset charset = StandardCharsets.US_ASCII; |
||||
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(charset)); |
||||
inputMessage.getHeaders().setContentType(new MediaType("application", "json", charset)); |
||||
HashMap<String, Object> result = (HashMap<String, Object>) this.converter.read(HashMap.class, inputMessage); |
||||
|
||||
assertThat(result).containsExactly(entry("foo", "bar")); |
||||
} |
||||
|
||||
@Test |
||||
void writeAscii() throws Exception { |
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); |
||||
Map<String,Object> body = new HashMap<>(); |
||||
body.put("foo", "bar"); |
||||
Charset charset = StandardCharsets.US_ASCII; |
||||
MediaType contentType = new MediaType("application", "json", charset); |
||||
converter.write(body, contentType, outputMessage); |
||||
|
||||
String result = outputMessage.getBodyAsString(charset); |
||||
assertThat(result).isEqualTo("{\"foo\":\"bar\"}"); |
||||
assertThat(outputMessage.getHeaders().getContentType()).as("Invalid content-type").isEqualTo(contentType); |
||||
} |
||||
|
||||
@Test |
||||
void readWithCustomized() throws IOException { |
||||
JacksonJsonHttpMessageConverterWithCustomization customizedConverter = |
||||
new JacksonJsonHttpMessageConverterWithCustomization(); |
||||
String body = "{\"property\":\"Value1\"}"; |
||||
MockHttpInputMessage inputMessage1 = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); |
||||
MockHttpInputMessage inputMessage2 = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); |
||||
inputMessage1.getHeaders().setContentType(new MediaType("application", "json")); |
||||
inputMessage2.getHeaders().setContentType(new MediaType("application", "json")); |
||||
|
||||
assertThatExceptionOfType(HttpMessageNotReadableException.class) |
||||
.isThrownBy(() -> customizedConverter.read(MyCustomizedBean.class, inputMessage1)); |
||||
|
||||
MyCustomizedBean customizedResult = (MyCustomizedBean) converter.read(MyCustomizedBean.class, inputMessage2); |
||||
assertThat(customizedResult.getProperty()).isEqualTo(MyCustomEnum.VAL1); |
||||
} |
||||
|
||||
@Test |
||||
void writeWithCustomized() throws IOException { |
||||
JacksonJsonHttpMessageConverterWithCustomization customizedConverter = |
||||
new JacksonJsonHttpMessageConverterWithCustomization(); |
||||
MockHttpOutputMessage outputMessage1 = new MockHttpOutputMessage(); |
||||
MockHttpOutputMessage outputMessage2 = new MockHttpOutputMessage(); |
||||
MyCustomizedBean body = new MyCustomizedBean(); |
||||
body.setProperty(MyCustomEnum.VAL2); |
||||
converter.write(body, null, outputMessage1); |
||||
customizedConverter.write(body, null, outputMessage2); |
||||
String result1 = outputMessage1.getBodyAsString(StandardCharsets.UTF_8); |
||||
assertThat(result1).contains("\"property\":\"Value2\""); |
||||
String result2 = outputMessage2.getBodyAsString(StandardCharsets.UTF_8); |
||||
assertThat(result2).contains("\"property\":\"VAL2\""); |
||||
} |
||||
|
||||
@Test |
||||
void repeatableWrites() throws IOException { |
||||
MockHttpOutputMessage outputMessage1 = new MockHttpOutputMessage(); |
||||
MyBean body = new MyBean(); |
||||
body.setString("Foo"); |
||||
converter.write(body, null, outputMessage1); |
||||
String result = outputMessage1.getBodyAsString(StandardCharsets.UTF_8); |
||||
assertThat(result).contains("\"string\":\"Foo\""); |
||||
|
||||
MockHttpOutputMessage outputMessage2 = new MockHttpOutputMessage(); |
||||
converter.write(body, null, outputMessage2); |
||||
result = outputMessage2.getBodyAsString(StandardCharsets.UTF_8); |
||||
assertThat(result).contains("\"string\":\"Foo\""); |
||||
} |
||||
|
||||
|
||||
|
||||
interface MyInterface { |
||||
|
||||
String getString(); |
||||
|
||||
void setString(String string); |
||||
} |
||||
|
||||
|
||||
public static class MyBase implements MyInterface { |
||||
|
||||
private String string; |
||||
|
||||
@Override |
||||
public String getString() { |
||||
return string; |
||||
} |
||||
|
||||
@Override |
||||
public void setString(String string) { |
||||
this.string = string; |
||||
} |
||||
} |
||||
|
||||
|
||||
public static class MyBean extends MyBase { |
||||
|
||||
private int number; |
||||
|
||||
private float fraction; |
||||
|
||||
private String[] array; |
||||
|
||||
private boolean bool; |
||||
|
||||
private byte[] bytes; |
||||
|
||||
public int getNumber() { |
||||
return number; |
||||
} |
||||
|
||||
public void setNumber(int number) { |
||||
this.number = number; |
||||
} |
||||
|
||||
public float getFraction() { |
||||
return fraction; |
||||
} |
||||
|
||||
public void setFraction(float fraction) { |
||||
this.fraction = fraction; |
||||
} |
||||
|
||||
public String[] getArray() { |
||||
return array; |
||||
} |
||||
|
||||
public void setArray(String[] array) { |
||||
this.array = array; |
||||
} |
||||
|
||||
public boolean isBool() { |
||||
return bool; |
||||
} |
||||
|
||||
public void setBool(boolean bool) { |
||||
this.bool = bool; |
||||
} |
||||
|
||||
public byte[] getBytes() { |
||||
return bytes; |
||||
} |
||||
|
||||
public void setBytes(byte[] bytes) { |
||||
this.bytes = bytes; |
||||
} |
||||
} |
||||
|
||||
|
||||
public static class PrettyPrintBean { |
||||
|
||||
private String name; |
||||
|
||||
public String getName() { |
||||
return name; |
||||
} |
||||
|
||||
public void setName(String name) { |
||||
this.name = name; |
||||
} |
||||
} |
||||
|
||||
|
||||
private interface MyJacksonView1 {} |
||||
|
||||
private interface MyJacksonView2 {} |
||||
|
||||
private interface MyJacksonView3 {} |
||||
|
||||
|
||||
@SuppressWarnings("unused") |
||||
@JsonView(MyJacksonView3.class) |
||||
private static class JacksonViewBean { |
||||
|
||||
@JsonView(MyJacksonView1.class) |
||||
private String withView1; |
||||
|
||||
@JsonView(MyJacksonView2.class) |
||||
private String withView2; |
||||
|
||||
private String withoutView; |
||||
|
||||
public String getWithView1() { |
||||
return withView1; |
||||
} |
||||
|
||||
public void setWithView1(String withView1) { |
||||
this.withView1 = withView1; |
||||
} |
||||
|
||||
public String getWithView2() { |
||||
return withView2; |
||||
} |
||||
|
||||
public void setWithView2(String withView2) { |
||||
this.withView2 = withView2; |
||||
} |
||||
|
||||
public String getWithoutView() { |
||||
return withoutView; |
||||
} |
||||
|
||||
public void setWithoutView(String withoutView) { |
||||
this.withoutView = withoutView; |
||||
} |
||||
} |
||||
|
||||
|
||||
@JsonFilter("myJacksonFilter") |
||||
@SuppressWarnings("unused") |
||||
private static class JacksonFilteredBean { |
||||
|
||||
private String property1; |
||||
|
||||
private String property2; |
||||
|
||||
public String getProperty1() { |
||||
return property1; |
||||
} |
||||
|
||||
public void setProperty1(String property1) { |
||||
this.property1 = property1; |
||||
} |
||||
|
||||
public String getProperty2() { |
||||
return property2; |
||||
} |
||||
|
||||
public void setProperty2(String property2) { |
||||
this.property2 = property2; |
||||
} |
||||
} |
||||
|
||||
|
||||
@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 property1; |
||||
} |
||||
|
||||
public String getProperty2() { |
||||
return property2; |
||||
} |
||||
} |
||||
|
||||
public static class MyCustomizedBean { |
||||
|
||||
private MyCustomEnum property; |
||||
|
||||
public MyCustomEnum getProperty() { |
||||
return property; |
||||
} |
||||
|
||||
public void setProperty(MyCustomEnum property) { |
||||
this.property = property; |
||||
} |
||||
} |
||||
|
||||
public enum MyCustomEnum { |
||||
VAL1, |
||||
VAL2; |
||||
|
||||
@Override |
||||
public String toString() { |
||||
return this == VAL1 ? "Value1" : "Value2"; |
||||
} |
||||
} |
||||
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME) |
||||
@JsonSubTypes(value = {@JsonSubTypes.Type(value = Impl1.class), |
||||
@JsonSubTypes.Type(value = Impl2.class)}) |
||||
public interface MyParent { |
||||
} |
||||
|
||||
public static class Impl1 implements MyParent { |
||||
} |
||||
|
||||
public static class Impl2 implements MyParent { |
||||
} |
||||
|
||||
private static class JacksonJsonHttpMessageConverterWithCustomization extends JacksonJsonHttpMessageConverter { |
||||
|
||||
@Override |
||||
protected ObjectReader customizeReader(ObjectReader reader, JavaType javaType) { |
||||
return reader.without(EnumFeature.READ_ENUMS_USING_TO_STRING); |
||||
} |
||||
|
||||
@Override |
||||
protected ObjectWriter customizeWriter(ObjectWriter writer, @Nullable JavaType javaType, @Nullable MediaType contentType) { |
||||
return writer.without(EnumFeature.WRITE_ENUMS_USING_TO_STRING); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,157 @@
@@ -0,0 +1,157 @@
|
||||
/* |
||||
* 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.converter.smile; |
||||
|
||||
import java.io.IOException; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
import tools.jackson.dataformat.smile.SmileMapper; |
||||
|
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.web.testfixture.http.MockHttpInputMessage; |
||||
import org.springframework.web.testfixture.http.MockHttpOutputMessage; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.within; |
||||
|
||||
/** |
||||
* Jackson 3.x Smile converter tests. |
||||
* |
||||
* @author Sebastien Deleuze |
||||
*/ |
||||
class JacksonSmileHttpMessageConverterTests { |
||||
|
||||
private final JacksonSmileHttpMessageConverter converter = new JacksonSmileHttpMessageConverter(); |
||||
private final SmileMapper mapper = SmileMapper.builder().build(); |
||||
|
||||
|
||||
@Test |
||||
void canRead() { |
||||
assertThat(converter.canRead(MyBean.class, new MediaType("application", "x-jackson-smile"))).isTrue(); |
||||
assertThat(converter.canRead(MyBean.class, new MediaType("application", "json"))).isFalse(); |
||||
assertThat(converter.canRead(MyBean.class, new MediaType("application", "xml"))).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
void canWrite() { |
||||
assertThat(converter.canWrite(MyBean.class, new MediaType("application", "x-jackson-smile"))).isTrue(); |
||||
assertThat(converter.canWrite(MyBean.class, new MediaType("application", "json"))).isFalse(); |
||||
assertThat(converter.canWrite(MyBean.class, new MediaType("application", "xml"))).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
void read() throws IOException { |
||||
MyBean body = new MyBean(); |
||||
body.setString("Foo"); |
||||
body.setNumber(42); |
||||
body.setFraction(42F); |
||||
body.setArray(new String[]{"Foo", "Bar"}); |
||||
body.setBool(true); |
||||
body.setBytes(new byte[]{0x1, 0x2}); |
||||
MockHttpInputMessage inputMessage = new MockHttpInputMessage(mapper.writeValueAsBytes(body)); |
||||
inputMessage.getHeaders().setContentType(new MediaType("application", "x-jackson-smile")); |
||||
MyBean result = (MyBean) converter.read(MyBean.class, inputMessage); |
||||
assertThat(result.getString()).isEqualTo("Foo"); |
||||
assertThat(result.getNumber()).isEqualTo(42); |
||||
assertThat(result.getFraction()).isCloseTo(42F, within(0F)); |
||||
|
||||
assertThat(result.getArray()).isEqualTo(new String[]{"Foo", "Bar"}); |
||||
assertThat(result.isBool()).isTrue(); |
||||
assertThat(result.getBytes()).isEqualTo(new byte[]{0x1, 0x2}); |
||||
} |
||||
|
||||
@Test |
||||
void write() throws IOException { |
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); |
||||
MyBean body = new MyBean(); |
||||
body.setString("Foo"); |
||||
body.setNumber(42); |
||||
body.setFraction(42F); |
||||
body.setArray(new String[]{"Foo", "Bar"}); |
||||
body.setBool(true); |
||||
body.setBytes(new byte[]{0x1, 0x2}); |
||||
converter.write(body, null, outputMessage); |
||||
assertThat(outputMessage.getBodyAsBytes()).isEqualTo(mapper.writeValueAsBytes(body)); |
||||
assertThat(outputMessage.getHeaders().getContentType()) |
||||
.as("Invalid content-type").isEqualTo(new MediaType("application", "x-jackson-smile")); |
||||
} |
||||
|
||||
|
||||
public static class MyBean { |
||||
|
||||
private String string; |
||||
|
||||
private int number; |
||||
|
||||
private float fraction; |
||||
|
||||
private String[] array; |
||||
|
||||
private boolean bool; |
||||
|
||||
private byte[] bytes; |
||||
|
||||
public byte[] getBytes() { |
||||
return bytes; |
||||
} |
||||
|
||||
public void setBytes(byte[] bytes) { |
||||
this.bytes = bytes; |
||||
} |
||||
|
||||
public boolean isBool() { |
||||
return bool; |
||||
} |
||||
|
||||
public void setBool(boolean bool) { |
||||
this.bool = bool; |
||||
} |
||||
|
||||
public String getString() { |
||||
return string; |
||||
} |
||||
|
||||
public void setString(String string) { |
||||
this.string = string; |
||||
} |
||||
|
||||
public int getNumber() { |
||||
return number; |
||||
} |
||||
|
||||
public void setNumber(int number) { |
||||
this.number = number; |
||||
} |
||||
|
||||
public float getFraction() { |
||||
return fraction; |
||||
} |
||||
|
||||
public void setFraction(float fraction) { |
||||
this.fraction = fraction; |
||||
} |
||||
|
||||
public String[] getArray() { |
||||
return array; |
||||
} |
||||
|
||||
public void setArray(String[] array) { |
||||
this.array = array; |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,319 @@
@@ -0,0 +1,319 @@
|
||||
/* |
||||
* 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.converter.xml; |
||||
|
||||
import java.io.IOException; |
||||
import java.nio.charset.Charset; |
||||
import java.nio.charset.StandardCharsets; |
||||
import java.util.Collections; |
||||
import java.util.Map; |
||||
|
||||
import com.fasterxml.jackson.annotation.JsonView; |
||||
import org.junit.jupiter.api.Test; |
||||
import tools.jackson.dataformat.xml.XmlMapper; |
||||
|
||||
import org.springframework.core.ResolvableType; |
||||
import org.springframework.core.io.ClassPathResource; |
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.http.converter.HttpMessageNotReadableException; |
||||
import org.springframework.web.testfixture.http.MockHttpInputMessage; |
||||
import org.springframework.web.testfixture.http.MockHttpOutputMessage; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType; |
||||
import static org.assertj.core.api.Assertions.within; |
||||
|
||||
/** |
||||
* Jackson 3.x XML converter tests. |
||||
* |
||||
* @author Sebastien Deleuze |
||||
*/ |
||||
class JacksonXmlHttpMessageConverterTests { |
||||
|
||||
private final JacksonXmlHttpMessageConverter converter = new JacksonXmlHttpMessageConverter(); |
||||
|
||||
|
||||
@Test |
||||
void canRead() { |
||||
assertThat(converter.canRead(MyBean.class, new MediaType("application", "xml"))).isTrue(); |
||||
assertThat(converter.canRead(MyBean.class, new MediaType("text", "xml"))).isTrue(); |
||||
assertThat(converter.canRead(MyBean.class, new MediaType("application", "soap+xml"))).isTrue(); |
||||
assertThat(converter.canRead(MyBean.class, new MediaType("text", "xml", StandardCharsets.UTF_8))).isTrue(); |
||||
assertThat(converter.canRead(MyBean.class, new MediaType("text", "xml", StandardCharsets.ISO_8859_1))).isTrue(); |
||||
} |
||||
|
||||
@Test |
||||
void canWrite() { |
||||
assertThat(converter.canWrite(MyBean.class, new MediaType("application", "xml"))).isTrue(); |
||||
assertThat(converter.canWrite(MyBean.class, new MediaType("text", "xml"))).isTrue(); |
||||
assertThat(converter.canWrite(MyBean.class, new MediaType("application", "soap+xml"))).isTrue(); |
||||
assertThat(converter.canWrite(MyBean.class, new MediaType("text", "xml", StandardCharsets.UTF_8))).isTrue(); |
||||
assertThat(converter.canWrite(MyBean.class, new MediaType("text", "xml", StandardCharsets.ISO_8859_1))).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
void read() throws IOException { |
||||
String body = "<MyBean>" + |
||||
"<string>Foo</string>" + |
||||
"<number>42</number>" + |
||||
"<fraction>42.0</fraction>" + |
||||
"<array><array>Foo</array>" + |
||||
"<array>Bar</array></array>" + |
||||
"<bool>true</bool>" + |
||||
"<bytes>AQI=</bytes></MyBean>"; |
||||
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); |
||||
inputMessage.getHeaders().setContentType(new MediaType("application", "xml")); |
||||
MyBean result = (MyBean) converter.read(MyBean.class, inputMessage); |
||||
assertThat(result.getString()).isEqualTo("Foo"); |
||||
assertThat(result.getNumber()).isEqualTo(42); |
||||
assertThat(result.getFraction()).isCloseTo(42F, within(0F)); |
||||
assertThat(result.getArray()).isEqualTo(new String[]{"Foo", "Bar"}); |
||||
assertThat(result.isBool()).isTrue(); |
||||
assertThat(result.getBytes()).isEqualTo(new byte[]{0x1, 0x2}); |
||||
} |
||||
|
||||
@Test |
||||
void write() throws IOException { |
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); |
||||
MyBean body = new MyBean(); |
||||
body.setString("Foo"); |
||||
body.setNumber(42); |
||||
body.setFraction(42F); |
||||
body.setArray(new String[]{"Foo", "Bar"}); |
||||
body.setBool(true); |
||||
body.setBytes(new byte[]{0x1, 0x2}); |
||||
converter.write(body, null, outputMessage); |
||||
String result = outputMessage.getBodyAsString(StandardCharsets.UTF_8); |
||||
assertThat(result).contains("<string>Foo</string>"); |
||||
assertThat(result).contains("<number>42</number>"); |
||||
assertThat(result).contains("<fraction>42.0</fraction>"); |
||||
assertThat(result).contains("<array><array>Foo</array><array>Bar</array></array>"); |
||||
assertThat(result).contains("<bool>true</bool>"); |
||||
assertThat(result).contains("<bytes>AQI=</bytes>"); |
||||
assertThat(outputMessage.getHeaders().getContentType()) |
||||
.as("Invalid content-type").isEqualTo(new MediaType("application", "xml", StandardCharsets.UTF_8)); |
||||
} |
||||
|
||||
@Test |
||||
void readInvalidXml() { |
||||
String body = "FooBar"; |
||||
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); |
||||
inputMessage.getHeaders().setContentType(MediaType.APPLICATION_XML); |
||||
assertThatExceptionOfType(HttpMessageNotReadableException.class).isThrownBy(() -> |
||||
converter.read(MyBean.class, inputMessage)); |
||||
} |
||||
|
||||
@Test |
||||
void readValidXmlWithUnknownProperty() throws IOException { |
||||
String body = "<MyBean><string>string</string><unknownProperty>value</unknownProperty></MyBean>"; |
||||
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); |
||||
inputMessage.getHeaders().setContentType(MediaType.APPLICATION_XML); |
||||
converter.read(MyBean.class, inputMessage); |
||||
// Assert no HttpMessageNotReadableException is thrown
|
||||
} |
||||
|
||||
@Test |
||||
void jsonView() throws Exception { |
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); |
||||
JacksonViewBean bean = new JacksonViewBean(); |
||||
bean.setWithView1("with"); |
||||
bean.setWithView2("with"); |
||||
bean.setWithoutView("without"); |
||||
|
||||
Map<String, Object> hints = Collections.singletonMap(JsonView.class.getName(), MyJacksonView1.class); |
||||
this.converter.write(bean, ResolvableType.forClass(JacksonViewBean.class), null, outputMessage, hints); |
||||
|
||||
String result = outputMessage.getBodyAsString(StandardCharsets.UTF_8); |
||||
assertThat(result).contains("<withView1>with</withView1>"); |
||||
assertThat(result).doesNotContain("<withView2>with</withView2>"); |
||||
assertThat(result).doesNotContain("<withoutView>without</withoutView>"); |
||||
} |
||||
|
||||
@Test |
||||
void customXmlMapper() { |
||||
new JacksonXmlHttpMessageConverter(new MyXmlMapper()); |
||||
// Assert no exception is thrown
|
||||
} |
||||
|
||||
@Test |
||||
void readWithExternalReference() throws IOException { |
||||
String body = "<!DOCTYPE MyBean SYSTEM \"https://192.168.28.42/1.jsp\" [" + |
||||
" <!ELEMENT root ANY >\n" + |
||||
" <!ENTITY ext SYSTEM \"" + |
||||
new ClassPathResource("external.txt", getClass()).getURI() + |
||||
"\" >]><MyBean><string>&ext;</string></MyBean>"; |
||||
|
||||
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); |
||||
inputMessage.getHeaders().setContentType(MediaType.APPLICATION_XML); |
||||
|
||||
assertThatExceptionOfType(HttpMessageNotReadableException.class).isThrownBy(() -> |
||||
this.converter.read(MyBean.class, inputMessage)); |
||||
} |
||||
|
||||
@Test |
||||
void readWithXmlBomb() { |
||||
// https://en.wikipedia.org/wiki/Billion_laughs
|
||||
// https://msdn.microsoft.com/en-us/magazine/ee335713.aspx
|
||||
String body = """ |
||||
<?xml version="1.0"?> |
||||
<!DOCTYPE lolz [ |
||||
<!ENTITY lol "lol"> |
||||
<!ELEMENT lolz (#PCDATA)> |
||||
<!ENTITY lol1 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;"> |
||||
<!ENTITY lol2 "&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;&lol1;"> |
||||
<!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;"> |
||||
<!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;"> |
||||
<!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;"> |
||||
<!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;"> |
||||
<!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;"> |
||||
<!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;"> |
||||
<!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;"> |
||||
]> |
||||
<MyBean>&lol9;</MyBean>"""; |
||||
|
||||
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(StandardCharsets.UTF_8)); |
||||
inputMessage.getHeaders().setContentType(MediaType.APPLICATION_XML); |
||||
|
||||
assertThatExceptionOfType(HttpMessageNotReadableException.class).isThrownBy(() -> |
||||
this.converter.read(MyBean.class, inputMessage)); |
||||
} |
||||
|
||||
@Test |
||||
void readNonUnicode() throws Exception { |
||||
String body = "<MyBean>" + |
||||
"<string>føø bår</string>" + |
||||
"</MyBean>"; |
||||
|
||||
Charset charset = StandardCharsets.ISO_8859_1; |
||||
MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes(charset)); |
||||
inputMessage.getHeaders().setContentType(new MediaType("application", "xml", charset)); |
||||
MyBean result = (MyBean) converter.read(MyBean.class, inputMessage); |
||||
assertThat(result.getString()).isEqualTo("føø bår"); |
||||
} |
||||
|
||||
|
||||
|
||||
public static class MyBean { |
||||
|
||||
private String string; |
||||
|
||||
private int number; |
||||
|
||||
private float fraction; |
||||
|
||||
private String[] array; |
||||
|
||||
private boolean bool; |
||||
|
||||
private byte[] bytes; |
||||
|
||||
public byte[] getBytes() { |
||||
return bytes; |
||||
} |
||||
|
||||
public void setBytes(byte[] bytes) { |
||||
this.bytes = bytes; |
||||
} |
||||
|
||||
public boolean isBool() { |
||||
return bool; |
||||
} |
||||
|
||||
public void setBool(boolean bool) { |
||||
this.bool = bool; |
||||
} |
||||
|
||||
public String getString() { |
||||
return string; |
||||
} |
||||
|
||||
public void setString(String string) { |
||||
this.string = string; |
||||
} |
||||
|
||||
public int getNumber() { |
||||
return number; |
||||
} |
||||
|
||||
public void setNumber(int number) { |
||||
this.number = number; |
||||
} |
||||
|
||||
public float getFraction() { |
||||
return fraction; |
||||
} |
||||
|
||||
public void setFraction(float fraction) { |
||||
this.fraction = fraction; |
||||
} |
||||
|
||||
public String[] getArray() { |
||||
return array; |
||||
} |
||||
|
||||
public void setArray(String[] array) { |
||||
this.array = array; |
||||
} |
||||
} |
||||
|
||||
|
||||
private interface MyJacksonView1 {} |
||||
|
||||
private interface MyJacksonView2 {} |
||||
|
||||
|
||||
@SuppressWarnings("unused") |
||||
private static class JacksonViewBean { |
||||
|
||||
@JsonView(MyJacksonView1.class) |
||||
private String withView1; |
||||
|
||||
@JsonView(MyJacksonView2.class) |
||||
private String withView2; |
||||
|
||||
private String withoutView; |
||||
|
||||
public String getWithView1() { |
||||
return withView1; |
||||
} |
||||
|
||||
public void setWithView1(String withView1) { |
||||
this.withView1 = withView1; |
||||
} |
||||
|
||||
public String getWithView2() { |
||||
return withView2; |
||||
} |
||||
|
||||
public void setWithView2(String withView2) { |
||||
this.withView2 = withView2; |
||||
} |
||||
|
||||
public String getWithoutView() { |
||||
return withoutView; |
||||
} |
||||
|
||||
public void setWithoutView(String withoutView) { |
||||
this.withoutView = withoutView; |
||||
} |
||||
} |
||||
|
||||
|
||||
private static class MyXmlMapper extends XmlMapper { |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,157 @@
@@ -0,0 +1,157 @@
|
||||
/* |
||||
* 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.converter.yaml; |
||||
|
||||
import java.io.IOException; |
||||
|
||||
import org.junit.jupiter.api.Test; |
||||
import tools.jackson.dataformat.yaml.YAMLMapper; |
||||
|
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.web.testfixture.http.MockHttpInputMessage; |
||||
import org.springframework.web.testfixture.http.MockHttpOutputMessage; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.within; |
||||
|
||||
/** |
||||
* Jackson 3.x YAML converter tests. |
||||
* |
||||
* @author Sebastien Deleuze |
||||
*/ |
||||
class JacksonYamlHttpMessageConverterTests { |
||||
|
||||
private final JacksonYamlHttpMessageConverter converter = new JacksonYamlHttpMessageConverter(); |
||||
private final YAMLMapper mapper = YAMLMapper.builder().build(); |
||||
|
||||
|
||||
@Test |
||||
void canRead() { |
||||
assertThat(converter.canRead(MyBean.class, MediaType.APPLICATION_YAML)).isTrue(); |
||||
assertThat(converter.canRead(MyBean.class, new MediaType("application", "json"))).isFalse(); |
||||
assertThat(converter.canRead(MyBean.class, new MediaType("application", "xml"))).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
void canWrite() { |
||||
assertThat(converter.canWrite(MyBean.class, MediaType.APPLICATION_YAML)).isTrue(); |
||||
assertThat(converter.canWrite(MyBean.class, new MediaType("application", "json"))).isFalse(); |
||||
assertThat(converter.canWrite(MyBean.class, new MediaType("application", "xml"))).isFalse(); |
||||
} |
||||
|
||||
@Test |
||||
void read() throws IOException { |
||||
MyBean body = new MyBean(); |
||||
body.setString("Foo"); |
||||
body.setNumber(42); |
||||
body.setFraction(42F); |
||||
body.setArray(new String[]{"Foo", "Bar"}); |
||||
body.setBool(true); |
||||
body.setBytes(new byte[]{0x1, 0x2}); |
||||
MockHttpInputMessage inputMessage = new MockHttpInputMessage(mapper.writeValueAsBytes(body)); |
||||
inputMessage.getHeaders().setContentType(MediaType.APPLICATION_YAML); |
||||
MyBean result = (MyBean) converter.read(MyBean.class, inputMessage); |
||||
assertThat(result.getString()).isEqualTo("Foo"); |
||||
assertThat(result.getNumber()).isEqualTo(42); |
||||
assertThat(result.getFraction()).isCloseTo(42F, within(0F)); |
||||
|
||||
assertThat(result.getArray()).isEqualTo(new String[]{"Foo", "Bar"}); |
||||
assertThat(result.isBool()).isTrue(); |
||||
assertThat(result.getBytes()).isEqualTo(new byte[]{0x1, 0x2}); |
||||
} |
||||
|
||||
@Test |
||||
void write() throws IOException { |
||||
MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); |
||||
MyBean body = new MyBean(); |
||||
body.setString("Foo"); |
||||
body.setNumber(42); |
||||
body.setFraction(42F); |
||||
body.setArray(new String[]{"Foo", "Bar"}); |
||||
body.setBool(true); |
||||
body.setBytes(new byte[]{0x1, 0x2}); |
||||
converter.write(body, null, outputMessage); |
||||
assertThat(outputMessage.getBodyAsBytes()).isEqualTo(mapper.writeValueAsBytes(body)); |
||||
assertThat(outputMessage.getHeaders().getContentType()) |
||||
.as("Invalid content-type").isEqualTo(MediaType.APPLICATION_YAML); |
||||
} |
||||
|
||||
|
||||
public static class MyBean { |
||||
|
||||
private String string; |
||||
|
||||
private int number; |
||||
|
||||
private float fraction; |
||||
|
||||
private String[] array; |
||||
|
||||
private boolean bool; |
||||
|
||||
private byte[] bytes; |
||||
|
||||
public byte[] getBytes() { |
||||
return bytes; |
||||
} |
||||
|
||||
public void setBytes(byte[] bytes) { |
||||
this.bytes = bytes; |
||||
} |
||||
|
||||
public boolean isBool() { |
||||
return bool; |
||||
} |
||||
|
||||
public void setBool(boolean bool) { |
||||
this.bool = bool; |
||||
} |
||||
|
||||
public String getString() { |
||||
return string; |
||||
} |
||||
|
||||
public void setString(String string) { |
||||
this.string = string; |
||||
} |
||||
|
||||
public int getNumber() { |
||||
return number; |
||||
} |
||||
|
||||
public void setNumber(int number) { |
||||
this.number = number; |
||||
} |
||||
|
||||
public float getFraction() { |
||||
return fraction; |
||||
} |
||||
|
||||
public void setFraction(float fraction) { |
||||
this.fraction = fraction; |
||||
} |
||||
|
||||
public String[] getArray() { |
||||
return array; |
||||
} |
||||
|
||||
public void setArray(String[] array) { |
||||
this.array = array; |
||||
} |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue