diff --git a/build.gradle b/build.gradle
index fd89bdc6e5..9974eff3b9 100644
--- a/build.gradle
+++ b/build.gradle
@@ -735,6 +735,7 @@ project("spring-web") {
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("javax.json.bind:javax.json.bind-api:1.0.0-M1")
optional("com.rometools:rome:${romeVersion}")
optional("org.eclipse.jetty:jetty-servlet:${jettyVersion}") {
exclude group: "javax.servlet", module: "javax.servlet-api"
diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/JsonbHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/JsonbHttpMessageConverter.java
new file mode 100644
index 0000000000..0dddf1e9fe
--- /dev/null
+++ b/spring-web/src/main/java/org/springframework/http/converter/json/JsonbHttpMessageConverter.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2002-2017 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.json;
+
+import java.io.Reader;
+import java.io.Writer;
+import java.lang.reflect.Type;
+import javax.json.bind.Jsonb;
+import javax.json.bind.JsonbBuilder;
+import javax.json.bind.JsonbConfig;
+
+import org.springframework.util.Assert;
+
+/**
+ * Implementation of {@link org.springframework.http.converter.HttpMessageConverter}
+ * that can read and write JSON using the
+ * JSON Binding API.
+ *
+ *
This converter can be used to bind to typed beans or untyped {@code HashMap}s.
+ * By default, it supports {@code application/json} and {@code application/*+json} with
+ * {@code UTF-8} character set.
+ *
+ * @author Juergen Hoeller
+ * @since 5.0
+ * @see javax.json.bind.Jsonb
+ * @see javax.json.bind.JsonbBuilder
+ * @see #setJsonb
+ */
+public class JsonbHttpMessageConverter extends AbstractJsonHttpMessageConverter {
+
+ private Jsonb jsonb;
+
+
+ /**
+ * Construct a new {@code JsonbHttpMessageConverter} with default configuration.
+ */
+ public JsonbHttpMessageConverter() {
+ this(JsonbBuilder.create());
+ }
+
+ /**
+ * Construct a new {@code JsonbHttpMessageConverter} with the given configuration.
+ * @param config the {@code JsonbConfig} for the underlying delegate
+ */
+ public JsonbHttpMessageConverter(JsonbConfig config) {
+ this(JsonbBuilder.create(config));
+ }
+
+ /**
+ * Construct a new {@code JsonbHttpMessageConverter} with the given delegate.
+ * @param jsonb the Jsonb instance to use
+ */
+ public JsonbHttpMessageConverter(Jsonb jsonb) {
+ setJsonb(jsonb);
+ }
+
+
+ /**
+ * Set the {@code Jsonb} instance to use.
+ * If not set, a default {@code Jsonb} instance will be created.
+ *
Setting a custom-configured {@code Jsonb} is one way to take further
+ * control of the JSON serialization process.
+ * @see #JsonbHttpMessageConverter(Jsonb)
+ * @see #JsonbHttpMessageConverter(JsonbConfig)
+ * @see JsonbBuilder
+ */
+ public void setJsonb(Jsonb jsonb) {
+ Assert.notNull(jsonb, "A Jsonb instance is required");
+ this.jsonb = jsonb;
+ }
+
+ /**
+ * Return the configured {@code Jsonb} instance for this converter.
+ */
+ public Jsonb getJsonb() {
+ return this.jsonb;
+ }
+
+
+ @Override
+ protected Object readInternal(Type resolvedType, Reader reader) throws Exception {
+ return getJsonb().fromJson(reader, resolvedType);
+ }
+
+ @Override
+ protected void writeInternal(Object o, Type type, Writer writer) throws Exception {
+ if (type != null) {
+ getJsonb().toJson(o, type, writer);
+ }
+ else {
+ getJsonb().toJson(o, writer);
+ }
+ }
+
+}
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 4583daceb7..6e4291cdbf 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2016 the original author or authors.
+ * Copyright 2002-2017 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.
@@ -18,6 +18,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.JsonbHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter;
import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter;
@@ -51,6 +52,9 @@ public class AllEncompassingFormHttpMessageConverter extends FormHttpMessageConv
private static final boolean gsonPresent =
ClassUtils.isPresent("com.google.gson.Gson", AllEncompassingFormHttpMessageConverter.class.getClassLoader());
+ private static final boolean jsonbPresent =
+ ClassUtils.isPresent("javax.json.bind.Jsonb", AllEncompassingFormHttpMessageConverter.class.getClassLoader());
+
public AllEncompassingFormHttpMessageConverter() {
addPartConverter(new SourceHttpMessageConverter<>());
@@ -65,6 +69,9 @@ public class AllEncompassingFormHttpMessageConverter extends FormHttpMessageConv
else if (gsonPresent) {
addPartConverter(new GsonHttpMessageConverter());
}
+ else if (jsonbPresent) {
+ addPartConverter(new JsonbHttpMessageConverter());
+ }
if (jackson2XmlPresent) {
addPartConverter(new MappingJackson2XmlHttpMessageConverter());
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 ea94a66c83..71d9a28627 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
@@ -44,6 +44,7 @@ import org.springframework.http.converter.cbor.MappingJackson2CborHttpMessageCon
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.JsonbHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter;
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
@@ -143,6 +144,9 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
private static final boolean gsonPresent =
ClassUtils.isPresent("com.google.gson.Gson", RestTemplate.class.getClassLoader());
+ private static final boolean jsonbPresent =
+ ClassUtils.isPresent("javax.json.bind.Jsonb", RestTemplate.class.getClassLoader());
+
private final List> messageConverters = new ArrayList<>();
@@ -182,6 +186,9 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat
else if (gsonPresent) {
this.messageConverters.add(new GsonHttpMessageConverter());
}
+ else if (jsonbPresent) {
+ this.messageConverters.add(new JsonbHttpMessageConverter());
+ }
if (jackson2SmilePresent) {
this.messageConverters.add(new MappingJackson2SmileHttpMessageConverter());
diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java
new file mode 100644
index 0000000000..e7e35989f6
--- /dev/null
+++ b/spring-web/src/test/java/org/springframework/http/converter/json/JsonbHttpMessageConverterTests.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright 2002-2017 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.json;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.Ignore;
+import org.junit.Test;
+
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.http.MediaType;
+import org.springframework.http.MockHttpInputMessage;
+import org.springframework.http.MockHttpOutputMessage;
+import org.springframework.http.converter.HttpMessageNotReadableException;
+
+import static org.junit.Assert.*;
+
+/**
+ * @author Juergen Hoeller
+ */
+@Ignore // until we are able to include Eclipse Yasson (the JSONB RI) in our build setup
+public class JsonbHttpMessageConverterTests {
+
+ private final JsonbHttpMessageConverter converter = new JsonbHttpMessageConverter();
+
+
+ @Test
+ public void canRead() {
+ assertTrue(this.converter.canRead(MyBean.class, new MediaType("application", "json")));
+ assertTrue(this.converter.canRead(Map.class, new MediaType("application", "json")));
+ }
+
+ @Test
+ public void canWrite() {
+ assertTrue(this.converter.canWrite(MyBean.class, new MediaType("application", "json")));
+ assertTrue(this.converter.canWrite(Map.class, new MediaType("application", "json")));
+ }
+
+ @Test
+ public void canReadAndWriteMicroformats() {
+ assertTrue(this.converter.canRead(MyBean.class, new MediaType("application", "vnd.test-micro-type+json")));
+ assertTrue(this.converter.canWrite(MyBean.class, new MediaType("application", "vnd.test-micro-type+json")));
+ }
+
+ @Test
+ public void readTyped() throws IOException {
+ String body = "{\"bytes\":[1,2],\"array\":[\"Foo\",\"Bar\"]," +
+ "\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}";
+ MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8"));
+ inputMessage.getHeaders().setContentType(new MediaType("application", "json"));
+ MyBean result = (MyBean) this.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
+ @SuppressWarnings("unchecked")
+ public void readUntyped() throws IOException {
+ String body = "{\"bytes\":[1,2],\"array\":[\"Foo\",\"Bar\"]," +
+ "\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}";
+ MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8"));
+ inputMessage.getHeaders().setContentType(new MediaType("application", "json"));
+ HashMap result = (HashMap) this.converter.read(HashMap.class, inputMessage);
+ assertEquals("Foo", result.get("string"));
+ Number n = (Number) result.get("number");
+ assertEquals(42, n.longValue());
+ n = (Number) result.get("fraction");
+ assertEquals(42D, n.doubleValue(), 0D);
+ List array = new ArrayList<>();
+ array.add("Foo");
+ array.add("Bar");
+ assertEquals(array, result.get("array"));
+ assertEquals(Boolean.TRUE, result.get("bool"));
+ byte[] bytes = new byte[2];
+ List resultBytes = (ArrayList)result.get("bytes");
+ for (int i = 0; i < 2; i++) {
+ bytes[i] = resultBytes.get(i).byteValue();
+ }
+ assertArrayEquals(new byte[] {0x1, 0x2}, bytes);
+ }
+
+ @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});
+ this.converter.write(body, null, outputMessage);
+ Charset utf8 = StandardCharsets.UTF_8;
+ String result = outputMessage.getBodyAsString(utf8);
+ assertTrue(result.contains("\"string\":\"Foo\""));
+ assertTrue(result.contains("\"number\":42"));
+ assertTrue(result.contains("fraction\":42.0"));
+ assertTrue(result.contains("\"array\":[\"Foo\",\"Bar\"]"));
+ assertTrue(result.contains("\"bool\":true"));
+ assertTrue(result.contains("\"bytes\":[1,2]"));
+ assertEquals("Invalid content-type", new MediaType("application", "json", utf8),
+ outputMessage.getHeaders().getContentType());
+ }
+
+ @Test
+ public void writeUTF16() throws IOException {
+ MediaType contentType = new MediaType("application", "json", StandardCharsets.UTF_16BE);
+ MockHttpOutputMessage outputMessage = new MockHttpOutputMessage();
+ String body = "H\u00e9llo W\u00f6rld";
+ this.converter.write(body, contentType, outputMessage);
+ assertEquals("Invalid result", "\"" + body + "\"", outputMessage.getBodyAsString(StandardCharsets.UTF_16BE));
+ assertEquals("Invalid content-type", contentType, outputMessage.getHeaders().getContentType());
+ }
+
+ @Test(expected = HttpMessageNotReadableException.class)
+ public void readInvalidJson() throws IOException {
+ String body = "FooBar";
+ MockHttpInputMessage inputMessage = new MockHttpInputMessage(body.getBytes("UTF-8"));
+ inputMessage.getHeaders().setContentType(new MediaType("application", "json"));
+ this.converter.read(MyBean.class, inputMessage);
+ }
+
+ @Test
+ @SuppressWarnings("unchecked")
+ public void readParameterizedType() throws IOException {
+ ParameterizedTypeReference> beansList = new ParameterizedTypeReference>() {
+ };
+
+ String body = "[{\"bytes\":[1,2],\"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 results = (List) converter.read(beansList.getType(), null, inputMessage);
+ assertEquals(1, results.size());
+ MyBean result = results.get(0);
+ 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());
+ }
+
+
+ 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/annotation/WebMvcConfigurationSupport.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/config/annotation/WebMvcConfigurationSupport.java
index 9ac1891058..49abd8574a 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
@@ -49,6 +49,7 @@ 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.JsonbHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.smile.MappingJackson2SmileHttpMessageConverter;
import org.springframework.http.converter.support.AllEncompassingFormHttpMessageConverter;
@@ -193,6 +194,9 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
private static final boolean gsonPresent =
ClassUtils.isPresent("com.google.gson.Gson", WebMvcConfigurationSupport.class.getClassLoader());
+ private static final boolean jsonbPresent =
+ ClassUtils.isPresent("javax.json.bind.Jsonb", WebMvcConfigurationSupport.class.getClassLoader());
+
private ApplicationContext applicationContext;
@@ -390,7 +394,7 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
if (jaxb2Present || jackson2XmlPresent) {
map.put("xml", MediaType.APPLICATION_XML);
}
- if (jackson2Present || gsonPresent) {
+ if (jackson2Present || gsonPresent || jsonbPresent) {
map.put("json", MediaType.APPLICATION_JSON);
}
if (jackson2SmilePresent) {
@@ -785,6 +789,9 @@ public class WebMvcConfigurationSupport implements ApplicationContextAware, Serv
else if (gsonPresent) {
messageConverters.add(new GsonHttpMessageConverter());
}
+ else if (jsonbPresent) {
+ messageConverters.add(new JsonbHttpMessageConverter());
+ }
if (jackson2SmilePresent) {
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.smile().applicationContext(this.applicationContext).build();