parent
9c0b28ffdc
commit
e5ff54955f
|
|
@ -80,9 +80,6 @@ public abstract class Jackson2CodecSupport {
|
|||
new MediaType("application", "*+json"),
|
||||
MediaType.APPLICATION_NDJSON);
|
||||
|
||||
private static final List<MimeType> problemDetailMimeTypes =
|
||||
Collections.singletonList(MediaType.APPLICATION_PROBLEM_JSON);
|
||||
|
||||
|
||||
protected final Log logger = HttpLogging.forLogName(getClass());
|
||||
|
||||
|
|
@ -186,7 +183,16 @@ public abstract class Jackson2CodecSupport {
|
|||
if (!CollectionUtils.isEmpty(result)) {
|
||||
return result;
|
||||
}
|
||||
return (ProblemDetail.class.isAssignableFrom(elementClass) ? problemDetailMimeTypes : getMimeTypes());
|
||||
return (ProblemDetail.class.isAssignableFrom(elementClass) ? getMediaTypesForProblemDetail() : getMimeTypes());
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the supported media type(s) for {@link ProblemDetail}.
|
||||
* By default, an empty list, unless overridden in subclasses.
|
||||
* @since 6.0.5
|
||||
*/
|
||||
protected List<MimeType> getMediaTypesForProblemDetail() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
protected boolean supportsMimeType(@Nullable MimeType mimeType) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
package org.springframework.http.codec.json;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
|
@ -46,6 +47,10 @@ import org.springframework.util.MimeType;
|
|||
*/
|
||||
public class Jackson2JsonEncoder extends AbstractJackson2Encoder {
|
||||
|
||||
private static final List<MimeType> problemDetailMimeTypes =
|
||||
Collections.singletonList(MediaType.APPLICATION_PROBLEM_JSON);
|
||||
|
||||
|
||||
@Nullable
|
||||
private final PrettyPrinter ssePrettyPrinter;
|
||||
|
||||
|
|
@ -68,6 +73,11 @@ public class Jackson2JsonEncoder extends AbstractJackson2Encoder {
|
|||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected List<MimeType> getMediaTypesForProblemDetail() {
|
||||
return problemDetailMimeTypes;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ObjectWriter customizeWriter(ObjectWriter writer, @Nullable MimeType mimeType,
|
||||
ResolvableType elementType, @Nullable Map<String, Object> hints) {
|
||||
|
|
|
|||
|
|
@ -91,9 +91,6 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
|
|||
ENCODINGS.put("US-ASCII", JsonEncoding.UTF8);
|
||||
}
|
||||
|
||||
private static final List<MediaType> problemDetailMediaTypes =
|
||||
Collections.singletonList(MediaType.APPLICATION_PROBLEM_JSON);
|
||||
|
||||
|
||||
protected ObjectMapper defaultObjectMapper;
|
||||
|
||||
|
|
@ -209,13 +206,23 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
|
|||
if (!CollectionUtils.isEmpty(result)) {
|
||||
return result;
|
||||
}
|
||||
return (ProblemDetail.class.isAssignableFrom(clazz) ? problemDetailMediaTypes : getSupportedMediaTypes());
|
||||
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.
|
||||
* @since 6.0.5
|
||||
*/
|
||||
protected List<MediaType> getMediaTypesForProblemDetail() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to use the {@link DefaultPrettyPrinter} when writing JSON.
|
||||
* This is a shortcut for setting up an {@code ObjectMapper} as follows:
|
||||
|
|
|
|||
|
|
@ -99,6 +99,10 @@ import org.springframework.util.xml.StaxUtils;
|
|||
*/
|
||||
public class Jackson2ObjectMapperBuilder {
|
||||
|
||||
private static boolean jackson2XmlPresent = ClassUtils.isPresent(
|
||||
"com.fasterxml.jackson.dataformat.xml.XmlMapper", Jackson2ObjectMapperBuilder.class.getClassLoader());
|
||||
|
||||
|
||||
private final Map<Class<?>, Class<?>> mixIns = new LinkedHashMap<>();
|
||||
|
||||
private final Map<Class<?>, JsonSerializer<?>> serializers = new LinkedHashMap<>();
|
||||
|
|
@ -755,7 +759,12 @@ public class Jackson2ObjectMapperBuilder {
|
|||
objectMapper.setFilterProvider(this.filters);
|
||||
}
|
||||
|
||||
objectMapper.addMixIn(ProblemDetail.class, ProblemDetailJacksonMixin.class);
|
||||
if (jackson2XmlPresent) {
|
||||
objectMapper.addMixIn(ProblemDetail.class, ProblemDetailJacksonXmlMixin.class);
|
||||
}
|
||||
else {
|
||||
objectMapper.addMixIn(ProblemDetail.class, ProblemDetailJacksonMixin.class);
|
||||
}
|
||||
this.mixIns.forEach(objectMapper::addMixIn);
|
||||
|
||||
if (!this.serializers.isEmpty() || !this.deserializers.isEmpty()) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
|
|
@ -17,6 +17,8 @@
|
|||
package org.springframework.http.converter.json;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
|
@ -45,6 +47,10 @@ import org.springframework.lang.Nullable;
|
|||
*/
|
||||
public class MappingJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {
|
||||
|
||||
private static final List<MediaType> problemDetailMediaTypes =
|
||||
Collections.singletonList(MediaType.APPLICATION_PROBLEM_JSON);
|
||||
|
||||
|
||||
@Nullable
|
||||
private String jsonPrefix;
|
||||
|
||||
|
|
@ -88,6 +94,11 @@ public class MappingJackson2HttpMessageConverter extends AbstractJackson2HttpMes
|
|||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected List<MediaType> getMediaTypesForProblemDetail() {
|
||||
return problemDetailMediaTypes;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void writePrefix(JsonGenerator generator, Object object) throws IOException {
|
||||
if (this.jsonPrefix != null) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
/*
|
||||
* Copyright 2002-2023 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.net.URI;
|
||||
import java.util.Map;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAnyGetter;
|
||||
import com.fasterxml.jackson.annotation.JsonAnySetter;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlProperty;
|
||||
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_EMPTY;
|
||||
|
||||
/**
|
||||
* Intended to be identical to {@link ProblemDetailJacksonMixin} but for used
|
||||
* instead of it when jackson-dataformat-xml is on the classpath. Customizes the
|
||||
* XML root element name and adds namespace information.
|
||||
*
|
||||
* <p>Note: Unfortunately, we cannot just use {@code JsonRootName} to specify
|
||||
* the namespace since that is not inherited by fields of the class. This is
|
||||
* why we need a dedicated mixin for use when jackson-dataformat-xml is on the
|
||||
* classpath. For more details, see
|
||||
* <a href="https://github.com/FasterXML/jackson-dataformat-xml/issues/355">FasterXML/jackson-dataformat-xml#355</a>.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 6.0.5
|
||||
*/
|
||||
@JsonInclude(NON_EMPTY)
|
||||
@JacksonXmlRootElement(localName = "problem", namespace = "urn:ietf:rfc:7807")
|
||||
public interface ProblemDetailJacksonXmlMixin {
|
||||
|
||||
@JacksonXmlProperty(namespace = "urn:ietf:rfc:7807")
|
||||
URI getType();
|
||||
|
||||
@JacksonXmlProperty(namespace = "urn:ietf:rfc:7807")
|
||||
String getTitle();
|
||||
|
||||
@JacksonXmlProperty(namespace = "urn:ietf:rfc:7807")
|
||||
int getStatus();
|
||||
|
||||
@JacksonXmlProperty(namespace = "urn:ietf:rfc:7807")
|
||||
String getDetail();
|
||||
|
||||
@JacksonXmlProperty(namespace = "urn:ietf:rfc:7807")
|
||||
URI getInstance();
|
||||
|
||||
@JsonAnySetter
|
||||
void setProperty(String name, @Nullable Object value);
|
||||
|
||||
@JsonAnyGetter
|
||||
@JacksonXmlProperty(namespace = "urn:ietf:rfc:7807")
|
||||
Map<String, Object> getProperties();
|
||||
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
|
|
@ -17,6 +17,8 @@
|
|||
package org.springframework.http.converter.xml;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
|
||||
|
|
@ -42,6 +44,10 @@ import org.springframework.util.Assert;
|
|||
*/
|
||||
public class MappingJackson2XmlHttpMessageConverter extends AbstractJackson2HttpMessageConverter {
|
||||
|
||||
private static final List<MediaType> problemDetailMediaTypes =
|
||||
Collections.singletonList(MediaType.APPLICATION_PROBLEM_XML);
|
||||
|
||||
|
||||
/**
|
||||
* Construct a new {@code MappingJackson2XmlHttpMessageConverter} using default configuration
|
||||
* provided by {@code Jackson2ObjectMapperBuilder}.
|
||||
|
|
@ -74,4 +80,9 @@ public class MappingJackson2XmlHttpMessageConverter extends AbstractJackson2Http
|
|||
super.setObjectMapper(objectMapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<MediaType> getMediaTypesForProblemDetail() {
|
||||
return problemDetailMediaTypes;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -371,7 +371,7 @@ class Jackson2ObjectMapperBuilderTests {
|
|||
.build();
|
||||
|
||||
assertThat(mapper.mixInCount()).isEqualTo(2);
|
||||
assertThat(mapper.findMixInClassFor(ProblemDetail.class)).isAssignableFrom(ProblemDetailJacksonMixin.class);
|
||||
assertThat(mapper.findMixInClassFor(ProblemDetail.class)).isAssignableFrom(ProblemDetailJacksonXmlMixin.class);
|
||||
assertThat(mapper.findMixInClassFor(target)).isSameAs(mixInSource);
|
||||
}
|
||||
|
||||
|
|
@ -387,7 +387,7 @@ class Jackson2ObjectMapperBuilderTests {
|
|||
.build();
|
||||
|
||||
assertThat(mapper.mixInCount()).isEqualTo(2);
|
||||
assertThat(mapper.findMixInClassFor(ProblemDetail.class)).isAssignableFrom(ProblemDetailJacksonMixin.class);
|
||||
assertThat(mapper.findMixInClassFor(ProblemDetail.class)).isAssignableFrom(ProblemDetailJacksonXmlMixin.class);
|
||||
assertThat(mapper.findMixInClassFor(target)).isSameAs(mixInSource);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -243,7 +243,7 @@ public class Jackson2ObjectMapperFactoryBeanTests {
|
|||
ObjectMapper mapper = this.factory.getObject();
|
||||
|
||||
assertThat(mapper.mixInCount()).isEqualTo(2);
|
||||
assertThat(mapper.findMixInClassFor(ProblemDetail.class)).isAssignableFrom(ProblemDetailJacksonMixin.class);
|
||||
assertThat(mapper.findMixInClassFor(ProblemDetail.class)).isAssignableFrom(ProblemDetailJacksonXmlMixin.class);
|
||||
assertThat(mapper.findMixInClassFor(target)).isSameAs(mixinSource);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ public class ProblemDetailJacksonMixinTests {
|
|||
|
||||
@Test
|
||||
void readCustomProperty() throws Exception {
|
||||
ProblemDetail problemDetail = this.mapper.readValue(
|
||||
ProblemDetail detail = this.mapper.readValue(
|
||||
"{\"type\":\"about:blank\"," +
|
||||
"\"title\":\"Bad Request\"," +
|
||||
"\"status\":400," +
|
||||
|
|
@ -73,14 +73,32 @@ public class ProblemDetailJacksonMixinTests {
|
|||
"\"host\":\"abc.org\"," +
|
||||
"\"user\":null}", ProblemDetail.class);
|
||||
|
||||
assertThat(problemDetail.getType()).isEqualTo(URI.create("about:blank"));
|
||||
assertThat(problemDetail.getTitle()).isEqualTo("Bad Request");
|
||||
assertThat(problemDetail.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||
assertThat(problemDetail.getDetail()).isEqualTo("Missing header");
|
||||
assertThat(problemDetail.getProperties()).containsEntry("host", "abc.org");
|
||||
assertThat(problemDetail.getProperties()).containsEntry("user", null);
|
||||
assertThat(detail.getType()).isEqualTo(URI.create("about:blank"));
|
||||
assertThat(detail.getTitle()).isEqualTo("Bad Request");
|
||||
assertThat(detail.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||
assertThat(detail.getDetail()).isEqualTo("Missing header");
|
||||
assertThat(detail.getProperties()).containsEntry("host", "abc.org");
|
||||
assertThat(detail.getProperties()).containsEntry("user", null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void readCustomPropertyFromXml() throws Exception {
|
||||
ObjectMapper xmlMapper = new Jackson2ObjectMapperBuilder().createXmlMapper(true).build();
|
||||
ProblemDetail detail = xmlMapper.readValue(
|
||||
"<problem xmlns=\"urn:ietf:rfc:7807\">" +
|
||||
"<type>about:blank</type>" +
|
||||
"<title>Bad Request</title>" +
|
||||
"<status>400</status>" +
|
||||
"<detail>Missing header</detail>" +
|
||||
"<host>abc.org</host>" +
|
||||
"</problem>", ProblemDetail.class);
|
||||
|
||||
assertThat(detail.getType()).isEqualTo(URI.create("about:blank"));
|
||||
assertThat(detail.getTitle()).isEqualTo("Bad Request");
|
||||
assertThat(detail.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||
assertThat(detail.getDetail()).isEqualTo("Missing header");
|
||||
assertThat(detail.getProperties()).containsEntry("host", "abc.org");
|
||||
}
|
||||
|
||||
private void testWrite(ProblemDetail problemDetail, String expected) throws Exception {
|
||||
String output = this.mapper.writeValueAsString(problemDetail);
|
||||
|
|
|
|||
|
|
@ -425,8 +425,8 @@ public class RequestResponseBodyMethodProcessorTests {
|
|||
this.servletRequest.setRequestURI("/path");
|
||||
|
||||
RequestResponseBodyMethodProcessor processor =
|
||||
new RequestResponseBodyMethodProcessor(
|
||||
Collections.singletonList(new MappingJackson2HttpMessageConverter()));
|
||||
new RequestResponseBodyMethodProcessor(List.of(
|
||||
new MappingJackson2HttpMessageConverter(), new MappingJackson2XmlHttpMessageConverter()));
|
||||
|
||||
MethodParameter returnType =
|
||||
new MethodParameter(getClass().getDeclaredMethod("handleAndReturnProblemDetail"), -1);
|
||||
|
|
@ -435,11 +435,29 @@ public class RequestResponseBodyMethodProcessorTests {
|
|||
|
||||
assertThat(this.servletResponse.getStatus()).isEqualTo(HttpStatus.BAD_REQUEST.value());
|
||||
assertThat(this.servletResponse.getContentType()).isEqualTo(expectedContentType);
|
||||
assertThat(this.servletResponse.getContentAsString()).isEqualTo(
|
||||
"{\"type\":\"about:blank\"," +
|
||||
"\"title\":\"Bad Request\"," +
|
||||
"\"status\":400," +
|
||||
"\"instance\":\"/path\"}");
|
||||
|
||||
if (expectedContentType.equals(MediaType.APPLICATION_PROBLEM_XML_VALUE)) {
|
||||
assertThat(this.servletResponse.getContentAsString()).isEqualTo(
|
||||
"<problem xmlns=\"urn:ietf:rfc:7807\">" +
|
||||
"<type>about:blank</type>" +
|
||||
"<title>Bad Request</title>" +
|
||||
"<status>400</status>" +
|
||||
"<instance>/path</instance>" +
|
||||
"</problem>");
|
||||
}
|
||||
else {
|
||||
assertThat(this.servletResponse.getContentAsString()).isEqualTo(
|
||||
"{\"type\":\"about:blank\"," +
|
||||
"\"title\":\"Bad Request\"," +
|
||||
"\"status\":400," +
|
||||
"\"instance\":\"/path\"}");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void problemDetailWhenProblemXmlRequested() throws Exception {
|
||||
this.servletRequest.addHeader("Accept", MediaType.APPLICATION_PROBLEM_XML_VALUE);
|
||||
testProblemDetailMediaType(MediaType.APPLICATION_PROBLEM_XML_VALUE);
|
||||
}
|
||||
|
||||
@Test // SPR-13135
|
||||
|
|
|
|||
Loading…
Reference in New Issue