diff --git a/build.gradle b/build.gradle index 31b56f7851..86f6258fa8 100644 --- a/build.gradle +++ b/build.gradle @@ -733,6 +733,8 @@ project("spring-web") { optional("com.squareup.okhttp3:okhttp:${okhttp3Version}") optional("com.fasterxml.jackson.core:jackson-databind:${jackson2Version}") optional("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:${jackson2Version}") + optional("com.fasterxml.jackson.dataformat:jackson-dataformat-smile:${jackson2Version}") + optional("com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:${jackson2Version}") optional("com.google.code.gson:gson:${gsonVersion}") optional("com.rometools:rome:${romeVersion}") optional("org.eclipse.jetty:jetty-servlet:${jettyVersion}") { @@ -855,6 +857,8 @@ project("spring-webmvc") { optional("com.lowagie:itext:2.1.7") optional("com.fasterxml.jackson.core:jackson-databind:${jackson2Version}") optional("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:${jackson2Version}") + optional("com.fasterxml.jackson.dataformat:jackson-dataformat-smile:${jackson2Version}") + optional("com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:${jackson2Version}") optional("com.rometools:rome:${romeVersion}") optional("javax.el:javax.el-api:${elApiVersion}") optional("org.apache.tiles:tiles-api:${tiles3Version}") diff --git a/spring-web/src/main/java/org/springframework/http/converter/cbor/MappingJackson2CborHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/cbor/MappingJackson2CborHttpMessageConverter.java new file mode 100644 index 0000000000..4a3dcb5e28 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/converter/cbor/MappingJackson2CborHttpMessageConverter.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2016 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 + * + * http://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 com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.cbor.CBORFactory; + +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.util.Assert; + +/** + * Implementation of {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverter} + * that can read and write CBOR data format using + * + * the dedicated Jackson 2.x extension. + * + *

By default, this converter supports {@code "application/cbor"} media type. This can be + * overridden by setting the {@link #setSupportedMediaTypes supportedMediaTypes} property. + * + *

The default constructor uses the default configuration provided by {@link Jackson2ObjectMapperBuilder}. + * + *

Compatible with Jackson 2.6 and higher. + * + * @author Sebastien Deleuze + * @since 5.0 + */ +public class MappingJackson2CborHttpMessageConverter extends AbstractJackson2HttpMessageConverter { + + /** + * Construct a new {@code MappingJackson2CborHttpMessageConverter} using default configuration + * provided by {@code Jackson2ObjectMapperBuilder}. + */ + public MappingJackson2CborHttpMessageConverter() { + this(Jackson2ObjectMapperBuilder.cbor().build()); + } + + /** + * Construct a new {@code MappingJackson2CborHttpMessageConverter} with a custom {@link ObjectMapper} + * (must be configured with a {@code CBORFactory} instance). + * You can use {@link Jackson2ObjectMapperBuilder} to build it easily. + * @see Jackson2ObjectMapperBuilder#cbor() + */ + public MappingJackson2CborHttpMessageConverter(ObjectMapper objectMapper) { + super(objectMapper, new MediaType("application", "cbor")); + Assert.isAssignable(CBORFactory.class, objectMapper.getFactory().getClass()); + } + + /** + * {@inheritDoc} + * The {@code objectMapper} parameter must be configured with a {@code CBORFactory} instance. + */ + @Override + public void setObjectMapper(ObjectMapper objectMapper) { + Assert.isAssignable(CBORFactory.class, objectMapper.getFactory().getClass()); + super.setObjectMapper(objectMapper); + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java index ff8e905a5c..e840fa2ff6 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilder.java @@ -31,6 +31,7 @@ import javax.xml.stream.XMLResolver; import com.fasterxml.jackson.annotation.JsonFilter; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.databind.AnnotationIntrospector; @@ -47,6 +48,8 @@ import com.fasterxml.jackson.databind.cfg.HandlerInstantiator; import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder; import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.FilterProvider; +import com.fasterxml.jackson.dataformat.cbor.CBORFactory; +import com.fasterxml.jackson.dataformat.smile.SmileFactory; import com.fasterxml.jackson.dataformat.xml.JacksonXmlModule; import com.fasterxml.jackson.dataformat.xml.XmlFactory; import com.fasterxml.jackson.dataformat.xml.XmlMapper; @@ -92,6 +95,8 @@ public class Jackson2ObjectMapperBuilder { private boolean createXmlMapper = false; + private JsonFactory factory; + private DateFormat dateFormat; private Locale locale; @@ -143,6 +148,16 @@ public class Jackson2ObjectMapperBuilder { return this; } + /** + * Define the {@link JsonFactory} to be used to create the {@link ObjectMapper} + * instance. + * @since 5.0 + */ + public Jackson2ObjectMapperBuilder factory(JsonFactory factory) { + this.factory = factory; + return this; + } + /** * Define the format for date/time with the given {@link DateFormat}. *

Note: Setting this property makes the exposed {@link ObjectMapper} @@ -585,7 +600,7 @@ public class Jackson2ObjectMapperBuilder { new XmlObjectMapperInitializer().create()); } else { - mapper = new ObjectMapper(); + mapper = (this.factory != null ? new ObjectMapper(this.factory) : new ObjectMapper()); } configure(mapper); return (T) mapper; @@ -794,6 +809,24 @@ public class Jackson2ObjectMapperBuilder { return new Jackson2ObjectMapperBuilder().createXmlMapper(true); } + /** + * Obtain a {@link Jackson2ObjectMapperBuilder} instance in order to + * build a Smile data format {@link ObjectMapper} instance. + * @since 5.0 + */ + public static Jackson2ObjectMapperBuilder smile() { + return new Jackson2ObjectMapperBuilder().factory(new SmileFactoryInitializer().create()); + } + + /** + * Obtain a {@link Jackson2ObjectMapperBuilder} instance in order to + * build a CBOR data format {@link ObjectMapper} instance. + * @since 5.0 + */ + public static Jackson2ObjectMapperBuilder cbor() { + return new Jackson2ObjectMapperBuilder().factory(new CborFactoryInitializer().create()); + } + private static class XmlObjectMapperInitializer { @@ -823,4 +856,16 @@ public class Jackson2ObjectMapperBuilder { }; } + private static class SmileFactoryInitializer { + public JsonFactory create() { + return new SmileFactory(); + } + } + + private static class CborFactoryInitializer { + public JsonFactory create() { + return new CBORFactory(); + } + } + } diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBean.java b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBean.java index d945a7fb99..ebfe418921 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBean.java +++ b/spring-web/src/main/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBean.java @@ -25,6 +25,7 @@ import java.util.TimeZone; import com.fasterxml.jackson.annotation.JsonFilter; import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonFactory; import com.fasterxml.jackson.databind.AnnotationIntrospector; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonDeserializer; @@ -163,6 +164,15 @@ public class Jackson2ObjectMapperFactoryBean implements FactoryBeanNote: Setting this property makes the exposed {@link ObjectMapper} diff --git a/spring-web/src/main/java/org/springframework/http/converter/smile/MappingJackson2SmileHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/smile/MappingJackson2SmileHttpMessageConverter.java new file mode 100644 index 0000000000..5d5040b85e --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/converter/smile/MappingJackson2SmileHttpMessageConverter.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2016 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 + * + * http://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 com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.smile.SmileFactory; + +import org.springframework.http.MediaType; +import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.util.Assert; + +/** + * Implementation of {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverter} + * that can read and write Smile data format ("binary JSON") using + * + * the dedicated Jackson 2.x extension. + * + *

By default, this converter supports {@code "application/x-jackson-smile"} media type. + * This can be overridden by setting the {@link #setSupportedMediaTypes supportedMediaTypes} property. + * + *

The default constructor uses the default configuration provided by {@link Jackson2ObjectMapperBuilder}. + * + *

Compatible with Jackson 2.6 and higher. + * + * @author Sebastien Deleuze + * @since 5.0 + */ +public class MappingJackson2SmileHttpMessageConverter extends AbstractJackson2HttpMessageConverter { + + /** + * Construct a new {@code MappingJackson2SmileHttpMessageConverter} using default configuration + * provided by {@code Jackson2ObjectMapperBuilder}. + */ + public MappingJackson2SmileHttpMessageConverter() { + this(Jackson2ObjectMapperBuilder.smile().build()); + } + + /** + * Construct a new {@code MappingJackson2SmileHttpMessageConverter} with a custom {@link ObjectMapper} + * (must be configured with a {@code SmileFactory} instance). + * You can use {@link Jackson2ObjectMapperBuilder} to build it easily. + * @see Jackson2ObjectMapperBuilder#smile() + */ + public MappingJackson2SmileHttpMessageConverter(ObjectMapper objectMapper) { + super(objectMapper, new MediaType("application", "x-jackson-smile")); + Assert.isAssignable(SmileFactory.class, objectMapper.getFactory().getClass()); + } + + /** + * {@inheritDoc} + * The {@code objectMapper} parameter must be configured with a {@code SmileFactory} instance. + */ + @Override + public void setObjectMapper(ObjectMapper objectMapper) { + Assert.isAssignable(SmileFactory.class, objectMapper.getFactory().getClass()); + super.setObjectMapper(objectMapper); + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java index e32b796dca..4583daceb7 100644 --- a/spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java +++ b/spring-web/src/main/java/org/springframework/http/converter/support/AllEncompassingFormHttpMessageConverter.java @@ -19,6 +19,7 @@ package org.springframework.http.converter.support; import org.springframework.http.converter.FormHttpMessageConverter; import org.springframework.http.converter.json.GsonHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter; import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; import org.springframework.http.converter.xml.SourceHttpMessageConverter; @@ -44,6 +45,9 @@ public class AllEncompassingFormHttpMessageConverter extends FormHttpMessageConv private static final boolean jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", AllEncompassingFormHttpMessageConverter.class.getClassLoader()); + private static final boolean jackson2SmilePresent = + ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", AllEncompassingFormHttpMessageConverter.class.getClassLoader()); + private static final boolean gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", AllEncompassingFormHttpMessageConverter.class.getClassLoader()); @@ -65,6 +69,10 @@ public class AllEncompassingFormHttpMessageConverter extends FormHttpMessageConv if (jackson2XmlPresent) { addPartConverter(new MappingJackson2XmlHttpMessageConverter()); } + + if (jackson2SmilePresent) { + addPartConverter(new MappingJackson2SmileHttpMessageConverter()); + } } } diff --git a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java index 2b0d0bf05b..45f52de57a 100644 --- a/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java +++ b/spring-web/src/main/java/org/springframework/web/client/RestTemplate.java @@ -40,10 +40,12 @@ import org.springframework.http.converter.GenericHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.ResourceHttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.converter.cbor.MappingJackson2CborHttpMessageConverter; import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter; import org.springframework.http.converter.feed.RssChannelHttpMessageConverter; import org.springframework.http.converter.json.GsonHttpMessageConverter; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter; import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; @@ -133,6 +135,12 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat private static final boolean jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", RestTemplate.class.getClassLoader()); + private static final boolean jackson2SmilePresent = + ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", RestTemplate.class.getClassLoader()); + + private static final boolean jackson2CborPresent = + ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", RestTemplate.class.getClassLoader()); + private static final boolean gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", RestTemplate.class.getClassLoader()); @@ -175,6 +183,13 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat else if (gsonPresent) { this.messageConverters.add(new GsonHttpMessageConverter()); } + + if (jackson2SmilePresent) { + this.messageConverters.add(new MappingJackson2SmileHttpMessageConverter()); + } + if (jackson2CborPresent) { + this.messageConverters.add(new MappingJackson2CborHttpMessageConverter()); + } } /** diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java index ec9910750b..41a42442af 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperBuilderTests.java @@ -63,6 +63,8 @@ import com.fasterxml.jackson.databind.ser.impl.SimpleFilterProvider; import com.fasterxml.jackson.databind.ser.std.ClassSerializer; import com.fasterxml.jackson.databind.ser.std.NumberSerializer; import com.fasterxml.jackson.databind.type.SimpleType; +import com.fasterxml.jackson.dataformat.cbor.CBORFactory; +import com.fasterxml.jackson.dataformat.smile.SmileFactory; import com.fasterxml.jackson.dataformat.xml.XmlMapper; import kotlin.ranges.IntRange; import org.joda.time.DateTime; @@ -455,6 +457,27 @@ public class Jackson2ObjectMapperBuilderTests { assertThat(output, containsString("foobar")); } + @Test // SPR-14435 + public void smile() { + ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.smile().build(); + assertNotNull(objectMapper); + assertEquals(SmileFactory.class, objectMapper.getFactory().getClass()); + } + + @Test // SPR-14435 + public void cbor() { + ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.cbor().build(); + assertNotNull(objectMapper); + assertEquals(CBORFactory.class, objectMapper.getFactory().getClass()); + } + + @Test // SPR-14435 + public void factory() { + ObjectMapper objectMapper = new Jackson2ObjectMapperBuilder().factory(new SmileFactory()).build(); + assertNotNull(objectMapper); + assertEquals(SmileFactory.class, objectMapper.getFactory().getClass()); + } + public static class CustomIntegerModule extends Module { diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBeanTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBeanTests.java index 094c516f14..baede479df 100644 --- a/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBeanTests.java +++ b/spring-web/src/test/java/org/springframework/http/converter/json/Jackson2ObjectMapperFactoryBeanTests.java @@ -28,6 +28,7 @@ import java.util.Locale; import java.util.Map; import java.util.TimeZone; +import com.fasterxml.jackson.dataformat.smile.SmileFactory; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; @@ -391,6 +392,16 @@ public class Jackson2ObjectMapperFactoryBeanTests { assertEquals(XmlMapper.class, this.factory.getObjectType()); } + @Test // SPR-14435 + public void setFactory() { + this.factory.setFactory(new SmileFactory()); + this.factory.afterPropertiesSet(); + + assertNotNull(this.factory.getObject()); + assertTrue(this.factory.isSingleton()); + assertEquals(SmileFactory.class, this.factory.getObject().getFactory().getClass()); + } + public static class CustomIntegerModule extends Module { diff --git a/spring-web/src/test/java/org/springframework/http/converter/smile/MappingJackson2SmileHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/smile/MappingJackson2SmileHttpMessageConverterTests.java new file mode 100644 index 0000000000..e875b72c2a --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/converter/smile/MappingJackson2SmileHttpMessageConverterTests.java @@ -0,0 +1,161 @@ +/* + * Copyright 2002-2016 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 + * + * http://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 java.nio.charset.StandardCharsets; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.smile.SmileFactory; +import static org.junit.Assert.*; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.http.MediaType; +import org.springframework.http.MockHttpInputMessage; +import org.springframework.http.MockHttpOutputMessage; + +/** + * Jackson 2.x Smile converter tests. + * + * @author Sebastien Deleuze + */ +public class MappingJackson2SmileHttpMessageConverterTests { + + private final MappingJackson2SmileHttpMessageConverter converter = new MappingJackson2SmileHttpMessageConverter(); + private final ObjectMapper mapper = new ObjectMapper(new SmileFactory()); + + @Rule + public ExpectedException thrown = ExpectedException.none(); + + + @Test + public void canRead() { + assertTrue(converter.canRead(MyBean.class, new MediaType("application", "x-jackson-smile"))); + assertFalse(converter.canRead(MyBean.class, new MediaType("application", "json"))); + assertFalse(converter.canRead(MyBean.class, new MediaType("application", "xml"))); + } + + @Test + public void canWrite() { + assertTrue(converter.canWrite(MyBean.class, new MediaType("application", "x-jackson-smile"))); + assertFalse(converter.canWrite(MyBean.class, new MediaType("application", "json"))); + assertFalse(converter.canWrite(MyBean.class, new MediaType("application", "xml"))); + } + + @Test + public 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); + assertEquals("Foo", result.getString()); + assertEquals(42, result.getNumber()); + assertEquals(42F, result.getFraction(), 0F); + assertArrayEquals(new String[]{"Foo", "Bar"}, result.getArray()); + assertTrue(result.isBool()); + assertArrayEquals(new byte[]{0x1, 0x2}, result.getBytes()); + } + + @Test + public 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); + assertArrayEquals(mapper.writeValueAsBytes(body), outputMessage.getBodyAsBytes()); + assertEquals("Invalid content-type", new MediaType("application", "x-jackson-smile", StandardCharsets.UTF_8), + outputMessage.getHeaders().getContentType()); + } + + + 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; + } + } + +} diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java index 6b37c5c029..8e2cddcd6b 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/AnnotationDrivenBeanDefinitionParser.java @@ -19,6 +19,8 @@ package org.springframework.web.servlet.config; import java.util.List; import java.util.Properties; +import com.fasterxml.jackson.dataformat.cbor.CBORFactory; +import com.fasterxml.jackson.dataformat.smile.SmileFactory; import org.w3c.dom.Element; import org.springframework.beans.factory.FactoryBean; @@ -43,11 +45,13 @@ import org.springframework.http.converter.ByteArrayHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.ResourceHttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.converter.cbor.MappingJackson2CborHttpMessageConverter; import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter; import org.springframework.http.converter.feed.RssChannelHttpMessageConverter; import org.springframework.http.converter.json.GsonHttpMessageConverter; import org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter; import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; @@ -171,6 +175,12 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { private static final boolean jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", AnnotationDrivenBeanDefinitionParser.class.getClassLoader()); + private static final boolean jackson2SmilePresent = + ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", AnnotationDrivenBeanDefinitionParser.class.getClassLoader()); + + private static final boolean jackson2CborPresent = + ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", AnnotationDrivenBeanDefinitionParser.class.getClassLoader()); + private static final boolean gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", AnnotationDrivenBeanDefinitionParser.class.getClassLoader()); @@ -431,6 +441,12 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { if (jackson2Present || gsonPresent) { props.put("json", MediaType.APPLICATION_JSON_VALUE); } + if (jackson2SmilePresent) { + props.put("smile", "application/x-jackson-smile"); + } + if (jackson2CborPresent) { + props.put("cbor", "application/cbor"); + } return props; } @@ -573,6 +589,21 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { else if (gsonPresent) { messageConverters.add(createConverterDefinition(GsonHttpMessageConverter.class, source)); } + + if (jackson2SmilePresent) { + RootBeanDefinition jacksonConverterDef = createConverterDefinition(MappingJackson2SmileHttpMessageConverter.class, source); + GenericBeanDefinition jacksonFactoryDef = createObjectMapperFactoryDefinition(source); + jacksonFactoryDef.getPropertyValues().add("factory", new SmileFactory()); + jacksonConverterDef.getConstructorArgumentValues().addIndexedArgumentValue(0, jacksonFactoryDef); + messageConverters.add(jacksonConverterDef); + } + if (jackson2CborPresent) { + RootBeanDefinition jacksonConverterDef = createConverterDefinition(MappingJackson2CborHttpMessageConverter.class, source); + GenericBeanDefinition jacksonFactoryDef = createObjectMapperFactoryDefinition(source); + jacksonFactoryDef.getPropertyValues().add("factory", new CBORFactory()); + jacksonConverterDef.getConstructorArgumentValues().addIndexedArgumentValue(0, jacksonFactoryDef); + messageConverters.add(jacksonConverterDef); + } } return messageConverters; } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java index fdb81aeb5a..4d5c4c2473 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java @@ -44,11 +44,13 @@ import org.springframework.http.converter.ByteArrayHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.ResourceHttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.http.converter.cbor.MappingJackson2CborHttpMessageConverter; import org.springframework.http.converter.feed.AtomFeedHttpMessageConverter; import org.springframework.http.converter.feed.RssChannelHttpMessageConverter; import org.springframework.http.converter.json.GsonHttpMessageConverter; import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter; import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter; import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; @@ -183,6 +185,12 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv private static final boolean jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", WebMvcConfigurationSupport.class.getClassLoader()); + private static final boolean jackson2SmilePresent = + ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", WebMvcConfigurationSupport.class.getClassLoader()); + + private static final boolean jackson2CborPresent = + ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", WebMvcConfigurationSupport.class.getClassLoader()); + private static final boolean gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", WebMvcConfigurationSupport.class.getClassLoader()); @@ -386,6 +394,12 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv if (jackson2Present || gsonPresent) { map.put("json", MediaType.APPLICATION_JSON); } + if (jackson2SmilePresent) { + map.put("smile", MediaType.valueOf("application/x-jackson-smile")); + } + if (jackson2CborPresent) { + map.put("cbor", MediaType.valueOf("application/cbor")); + } return map; } @@ -775,6 +789,15 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv else if (gsonPresent) { messageConverters.add(new GsonHttpMessageConverter()); } + + if (jackson2SmilePresent) { + ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.smile().applicationContext(this.applicationContext).build(); + messageConverters.add(new MappingJackson2SmileHttpMessageConverter(objectMapper)); + } + if (jackson2CborPresent) { + ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.cbor().applicationContext(this.applicationContext).build(); + messageConverters.add(new MappingJackson2CborHttpMessageConverter(objectMapper)); + } } /** diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java index 60de6e8308..96177ee9d3 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupportTests.java @@ -175,7 +175,7 @@ public class WebMvcConfigurationSupportTests { ApplicationContext context = initContext(WebConfig.class); RequestMappingHandlerAdapter adapter = context.getBean(RequestMappingHandlerAdapter.class); List> converters = adapter.getMessageConverters(); - assertEquals(9, converters.size()); + assertEquals(11, converters.size()); converters.stream() .filter(converter -> converter instanceof AbstractJackson2HttpMessageConverter) .forEach(converter -> { diff --git a/src/asciidoc/web-mvc.adoc b/src/asciidoc/web-mvc.adoc index 0663c768d7..62f7a73af5 100644 --- a/src/asciidoc/web-mvc.adoc +++ b/src/asciidoc/web-mvc.adoc @@ -4829,6 +4829,12 @@ is present on the classpath. .. `MappingJackson2XmlHttpMessageConverter` converts to/from XML -- added if https://github.com/FasterXML/jackson-dataformat-xml[Jackson 2 XML extension] is present on the classpath. +.. `MappingJackson2SmileHttpMessageConverter` converts to/from Smile (binary JSON) -- added if +https://github.com/FasterXML/jackson-dataformats-binary/tree/master/smile[Jackson 2 Smile extension] +is present on the classpath. +.. `MappingJackson2CborHttpMessageConverter` converts to/from CBOR -- added if +https://github.com/FasterXML/jackson-dataformats-binary/tree/master/cbor[Jackson 2 CBOR extension] +is present on the classpath. .. `AtomFeedHttpMessageConverter` converts Atom feeds -- added if Rome is present on the classpath. .. `RssChannelHttpMessageConverter` converts RSS feeds -- added if Rome is present on