Support for JSON Binding API (JSON-B)

Issue: SPR-14923
This commit is contained in:
Juergen Hoeller
2017-04-10 15:36:38 +02:00
parent e5fdd4cd1d
commit cf306037b5
6 changed files with 367 additions and 2 deletions

View File

@@ -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"

View File

@@ -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
* <a href="http://json-b.net/">JSON Binding API</a>.
*
* <p>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.
* <p>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);
}
}
}

View File

@@ -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());

View File

@@ -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<HttpMessageConverter<?>> 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());

View File

@@ -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<String, Object> result = (HashMap<String, Object>) 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<String> 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<Number> resultBytes = (ArrayList<Number>)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<List<MyBean>> beansList = new ParameterizedTypeReference<List<MyBean>>() {
};
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<MyBean> results = (List<MyBean>) 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;
}
}
}

View File

@@ -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();