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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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 @@ |
|||||||
|
/* |
||||||
|
* 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