Browse Source

Fix wildcard MIME type support in messaging converters

Prior to this commit, the "application/*+json" wildcard MIME type was
added to the list of supported MIME types in the JSON messaging
converter. This change wasn't fully reflected in the
`AbstractMessageConverter`, because only strict matching of type and
subtybe were considered.

This commit updates the `AbstractMessageConverter` to not only check the
type and subtype, but also check whether the supported MIME type
includes the one given as a parameter.

Fixes gh-36285
pull/36293/head
Brian Clozel 1 month ago
parent
commit
e93e55b456
  1. 2
      spring-messaging/src/main/java/org/springframework/messaging/converter/AbstractMessageConverter.java
  2. 379
      spring-messaging/src/test/java/org/springframework/messaging/converter/JacksonJsonMessageConverterTests.java
  3. 20
      spring-messaging/src/test/java/org/springframework/messaging/converter/MappingJackson2MessageConverterTests.java

2
spring-messaging/src/main/java/org/springframework/messaging/converter/AbstractMessageConverter.java

@ -236,7 +236,7 @@ public abstract class AbstractMessageConverter implements SmartMessageConverter @@ -236,7 +236,7 @@ public abstract class AbstractMessageConverter implements SmartMessageConverter
return !isStrictContentTypeMatch();
}
for (MimeType current : getSupportedMimeTypes()) {
if (current.getType().equals(mimeType.getType()) && current.getSubtype().equals(mimeType.getSubtype())) {
if (current.includes(mimeType)) {
return true;
}
}

379
spring-messaging/src/test/java/org/springframework/messaging/converter/JacksonJsonMessageConverterTests.java

@ -0,0 +1,379 @@ @@ -0,0 +1,379 @@
/*
* Copyright 2002-present 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.messaging.converter;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.fasterxml.jackson.annotation.JsonView;
import org.junit.jupiter.api.Test;
import tools.jackson.databind.DeserializationFeature;
import org.springframework.core.MethodParameter;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.util.MimeType;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.within;
/**
* Tests for {@link JacksonJsonMessageConverter}.
*
* @author Brian Clozel
* @author Rossen Stoyanchev
* @author Sebastien Deleuze
*/
class JacksonJsonMessageConverterTests {
@Test
void defaultConstructor() {
JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter();
assertThat(converter.getSupportedMimeTypes()).contains(new MimeType("application", "json"));
assertThat(converter.getJsonMapper().deserializationConfig()
.isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)).isFalse();
}
@Test // SPR-12724
void mimetypeParametrizedConstructor() {
MimeType mimetype = new MimeType("application", "xml", StandardCharsets.UTF_8);
JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter(mimetype);
assertThat(converter.getSupportedMimeTypes()).contains(mimetype);
assertThat(converter.getJsonMapper().deserializationConfig()
.isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)).isFalse();
}
@Test // SPR-12724
void mimetypesParametrizedConstructor() {
MimeType jsonMimetype = new MimeType("application", "json", StandardCharsets.UTF_8);
MimeType xmlMimetype = new MimeType("application", "xml", StandardCharsets.UTF_8);
JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter(jsonMimetype, xmlMimetype);
assertThat(converter.getSupportedMimeTypes()).contains(jsonMimetype, xmlMimetype);
assertThat(converter.getJsonMapper().deserializationConfig()
.isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)).isFalse();
}
@Test
void supportJsonMimeType() {
JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter();
Message<String> message = MessageBuilder.withPayload("foo")
.setHeader(MessageHeaders.CONTENT_TYPE, "application/json").build();
assertThat(converter.supportsMimeType(message.getHeaders())).isTrue();
}
@Test
void supportVendorJsonMimeTypes() {
JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter();
Message<String> message = MessageBuilder.withPayload("foo")
.setHeader(MessageHeaders.CONTENT_TYPE, "application/vnd.springframework.type+json").build();
assertThat(converter.supportsMimeType(message.getHeaders())).isTrue();
}
@Test
void fromMessage() {
JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter();
String payload = "{\"bytes\":\"AQI=\",\"array\":[\"Foo\",\"Bar\"]," +
"\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}";
Message<?> message = MessageBuilder.withPayload(payload.getBytes(StandardCharsets.UTF_8)).build();
MyBean actual = (MyBean) converter.fromMessage(message, MyBean.class);
assertThat(actual.getString()).isEqualTo("Foo");
assertThat(actual.getNumber()).isEqualTo(42);
assertThat(actual.getFraction()).isCloseTo(42F, within(0F));
assertThat(actual.getArray()).isEqualTo(new String[]{"Foo", "Bar"});
assertThat(actual.isBool()).isTrue();
assertThat(actual.getBytes()).isEqualTo(new byte[]{0x1, 0x2});
}
@Test
void fromMessageUntyped() {
JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter();
String payload = "{\"bytes\":\"AQI=\",\"array\":[\"Foo\",\"Bar\"]," +
"\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}";
Message<?> message = MessageBuilder.withPayload(payload.getBytes(StandardCharsets.UTF_8)).build();
@SuppressWarnings("unchecked")
HashMap<String, Object> actual = (HashMap<String, Object>) converter.fromMessage(message, HashMap.class);
assertThat(actual.get("string")).isEqualTo("Foo");
assertThat(actual.get("number")).isEqualTo(42);
assertThat((Double) actual.get("fraction")).isCloseTo(42D, within(0D));
assertThat(actual.get("array")).isEqualTo(Arrays.asList("Foo", "Bar"));
assertThat(actual.get("bool")).isEqualTo(Boolean.TRUE);
assertThat(actual.get("bytes")).isEqualTo("AQI=");
}
@Test // gh-22386
public void fromMessageMatchingInstance() {
MyBean myBean = new MyBean();
JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter();
Message<?> message = MessageBuilder.withPayload(myBean).build();
assertThat(converter.fromMessage(message, MyBean.class)).isSameAs(myBean);
}
@Test
void fromMessageInvalidJson() {
JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter();
String payload = "FooBar";
Message<?> message = MessageBuilder.withPayload(payload.getBytes(StandardCharsets.UTF_8)).build();
assertThatExceptionOfType(MessageConversionException.class).isThrownBy(() ->
converter.fromMessage(message, MyBean.class));
}
@Test
void fromMessageValidJsonWithUnknownProperty() {
JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter();
String payload = "{\"string\":\"string\",\"unknownProperty\":\"value\"}";
Message<?> message = MessageBuilder.withPayload(payload.getBytes(StandardCharsets.UTF_8)).build();
MyBean myBean = (MyBean)converter.fromMessage(message, MyBean.class);
assertThat(myBean.getString()).isEqualTo("string");
}
@Test // SPR-16252
public void fromMessageToList() throws Exception {
JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter();
String payload = "[1, 2, 3, 4, 5, 6, 7, 8, 9]";
Message<?> message = MessageBuilder.withPayload(payload.getBytes(StandardCharsets.UTF_8)).build();
Method method = getClass().getDeclaredMethod("handleList", List.class);
MethodParameter param = new MethodParameter(method, 0);
Object actual = converter.fromMessage(message, List.class, param);
assertThat(actual).isNotNull();
assertThat(actual).isEqualTo(Arrays.asList(1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L));
}
@Test // SPR-16486
public void fromMessageToMessageWithPojo() throws Exception {
JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter();
String payload = "{\"string\":\"foo\"}";
Message<?> message = MessageBuilder.withPayload(payload.getBytes(StandardCharsets.UTF_8)).build();
Method method = getClass().getDeclaredMethod("handleMessage", Message.class);
MethodParameter param = new MethodParameter(method, 0);
Object actual = converter.fromMessage(message, MyBean.class, param);
assertThat(actual).isInstanceOf(MyBean.class);
assertThat(((MyBean) actual).getString()).isEqualTo("foo");
}
@Test
void toMessage() {
JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter();
MyBean payload = new MyBean();
payload.setString("Foo");
payload.setNumber(42);
payload.setFraction(42F);
payload.setArray(new String[]{"Foo", "Bar"});
payload.setBool(true);
payload.setBytes(new byte[]{0x1, 0x2});
Message<?> message = converter.toMessage(payload, null);
String actual = new String((byte[]) message.getPayload(), StandardCharsets.UTF_8);
assertThat(actual).contains("\"string\":\"Foo\"");
assertThat(actual).contains("\"number\":42");
assertThat(actual).contains("fraction\":42.0");
assertThat(actual).contains("\"array\":[\"Foo\",\"Bar\"]");
assertThat(actual).contains("\"bool\":true");
assertThat(actual).contains("\"bytes\":\"AQI=\"");
assertThat(message.getHeaders().get(MessageHeaders.CONTENT_TYPE, MimeType.class)).as("Invalid content-type").isEqualTo(new MimeType("application", "json"));
}
@Test
void toMessageUtf16() {
JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter();
MimeType contentType = new MimeType("application", "json", StandardCharsets.UTF_16BE);
Map<String, Object> map = new HashMap<>();
map.put(MessageHeaders.CONTENT_TYPE, contentType);
MessageHeaders headers = new MessageHeaders(map);
String payload = "H\u00e9llo W\u00f6rld";
Message<?> message = converter.toMessage(payload, headers);
assertThat(new String((byte[]) message.getPayload(), StandardCharsets.UTF_16BE)).isEqualTo("\"" + payload + "\"");
assertThat(message.getHeaders().get(MessageHeaders.CONTENT_TYPE)).isEqualTo(contentType);
}
@Test
void toMessageUtf16String() {
JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter();
converter.setSerializedPayloadClass(String.class);
MimeType contentType = new MimeType("application", "json", StandardCharsets.UTF_16BE);
Map<String, Object> map = new HashMap<>();
map.put(MessageHeaders.CONTENT_TYPE, contentType);
MessageHeaders headers = new MessageHeaders(map);
String payload = "H\u00e9llo W\u00f6rld";
Message<?> message = converter.toMessage(payload, headers);
assertThat(message.getPayload()).isEqualTo("\"" + payload + "\"");
assertThat(message.getHeaders().get(MessageHeaders.CONTENT_TYPE)).isEqualTo(contentType);
}
@Test
void toMessageJsonView() throws Exception {
JacksonJsonMessageConverter converter = new JacksonJsonMessageConverter();
Map<String, Object> map = new HashMap<>();
Method method = getClass().getDeclaredMethod("jsonViewResponse");
MethodParameter returnType = new MethodParameter(method, -1);
Message<?> message = converter.toMessage(jsonViewResponse(), new MessageHeaders(map), returnType);
String actual = new String((byte[]) message.getPayload(), StandardCharsets.UTF_8);
assertThat(actual).contains("\"withView1\":\"with\"");
assertThat(actual).contains("\"withView2\":\"with\"");
assertThat(actual).doesNotContain("\"withoutView\":\"with\"");
method = getClass().getDeclaredMethod("jsonViewPayload", JacksonViewBean.class);
MethodParameter param = new MethodParameter(method, 0);
JacksonViewBean back = (JacksonViewBean) converter.fromMessage(message, JacksonViewBean.class, param);
assertThat(back.getWithView1()).isNull();
assertThat(back.getWithView2()).isEqualTo("with");
assertThat(back.getWithoutView()).isNull();
}
@JsonView(MyJacksonView1.class)
public JacksonViewBean jsonViewResponse() {
JacksonViewBean bean = new JacksonViewBean();
bean.setWithView1("with");
bean.setWithView2("with");
bean.setWithoutView("with");
return bean;
}
public void jsonViewPayload(@JsonView(MyJacksonView2.class) JacksonViewBean payload) {
}
void handleList(List<Long> payload) {
}
void handleMessage(Message<MyBean> message) {
}
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;
}
}
public interface MyJacksonView1 {}
public interface MyJacksonView2 {}
public static class JacksonViewBean {
@JsonView(MyJacksonView1.class)
private String withView1;
@JsonView({MyJacksonView1.class, 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;
}
}
}

20
spring-messaging/src/test/java/org/springframework/messaging/converter/MappingJackson2MessageConverterTests.java

@ -55,7 +55,7 @@ class MappingJackson2MessageConverterTests { @@ -55,7 +55,7 @@ class MappingJackson2MessageConverterTests {
}
@Test // SPR-12724
public void mimetypeParametrizedConstructor() {
void mimetypeParametrizedConstructor() {
MimeType mimetype = new MimeType("application", "xml", StandardCharsets.UTF_8);
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter(mimetype);
assertThat(converter.getSupportedMimeTypes()).contains(mimetype);
@ -64,7 +64,7 @@ class MappingJackson2MessageConverterTests { @@ -64,7 +64,7 @@ class MappingJackson2MessageConverterTests {
}
@Test // SPR-12724
public void mimetypesParametrizedConstructor() {
void mimetypesParametrizedConstructor() {
MimeType jsonMimetype = new MimeType("application", "json", StandardCharsets.UTF_8);
MimeType xmlMimetype = new MimeType("application", "xml", StandardCharsets.UTF_8);
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter(jsonMimetype, xmlMimetype);
@ -73,6 +73,22 @@ class MappingJackson2MessageConverterTests { @@ -73,6 +73,22 @@ class MappingJackson2MessageConverterTests {
.isEnabled(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)).isFalse();
}
@Test
void supportJsonMimeType() {
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
Message<String> message = MessageBuilder.withPayload("foo")
.setHeader(MessageHeaders.CONTENT_TYPE, "application/json").build();
assertThat(converter.supportsMimeType(message.getHeaders())).isTrue();
}
@Test
void supportVendorJsonMimeTypes() {
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();
Message<String> message = MessageBuilder.withPayload("foo")
.setHeader(MessageHeaders.CONTENT_TYPE, "application/vnd.springframework.type+json").build();
assertThat(converter.supportsMimeType(message.getHeaders())).isTrue();
}
@Test
void fromMessage() {
MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter();

Loading…
Cancel
Save