diff --git a/build.gradle b/build.gradle index 0486bf308ef2..b7840db8437e 100644 --- a/build.gradle +++ b/build.gradle @@ -22,6 +22,7 @@ configure(allprojects) { project -> ext.hibVal5Version = "5.1.1.Final" ext.hsqldbVersion = "2.3.2" ext.jackson2Version = "2.3.3" + ext.gsonVersion = "2.2.4" ext.jasperReportsVersion = "5.5.2" ext.jettyVersion = "9.1.5.v20140505" ext.jodaVersion = "2.3" @@ -621,6 +622,8 @@ project("spring-web") { optional("org.apache.httpcomponents:httpclient:4.3.3") optional("org.apache.httpcomponents:httpasyncclient:4.0.1") optional("com.fasterxml.jackson.core:jackson-databind:${jackson2Version}") + optional("com.google.code.gson:gson:${gsonVersion}") + optional("commons-codec:commons-codec:1.9") optional("rome:rome:1.0") 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/GsonBase64ByteArrayJsonTypeAdapter.java b/spring-web/src/main/java/org/springframework/http/converter/json/GsonBase64ByteArrayJsonTypeAdapter.java new file mode 100644 index 000000000000..a232af6dcc73 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/converter/json/GsonBase64ByteArrayJsonTypeAdapter.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2014 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.lang.reflect.Type; +import java.nio.charset.Charset; + +import org.apache.commons.codec.binary.Base64; + +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonParseException; +import com.google.gson.JsonPrimitive; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; +import com.google.gson.TypeAdapter; + +/** + * Custom Gson {@link TypeAdapter} for serialization or deserialization of + * {@code byte[]}. By default Gson converts byte arrays to JSON arrays instead + * of a Base64 encoded string. Use this type adapter with + * {@link org.springframework.http.converter.json.GsonHttpMessageConverter + * GsonHttpMessageConverter} to read and write Base64 encoded byte arrays. + * + * @author Roy Clarkson + * @since 4.1 + * @see GsonBuilder#registerTypeHierarchyAdapter(Class, Object) + */ +final class GsonBase64ByteArrayJsonTypeAdapter implements JsonSerializer, JsonDeserializer { + + public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); + + private final Base64 base64 = new Base64(); + + + @Override + public JsonElement serialize(byte[] src, Type typeOfSrc, JsonSerializationContext context) { + return new JsonPrimitive(new String(this.base64.encode(src), DEFAULT_CHARSET)); + } + + @Override + public byte[] deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) + throws JsonParseException { + + return this.base64.decode(json.getAsString().getBytes(DEFAULT_CHARSET)); + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/GsonFactoryBean.java b/spring-web/src/main/java/org/springframework/http/converter/json/GsonFactoryBean.java new file mode 100644 index 000000000000..44968e99a37a --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/converter/json/GsonFactoryBean.java @@ -0,0 +1,215 @@ +/* + * Copyright 2002-2014 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.text.SimpleDateFormat; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.util.ClassUtils; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + + +/** + * A {@link FactoryBean} for creating a Google Gson 2.x {@link Gson} + * + * @author Roy Clarkson + * @since 4.1 + */ +public class GsonFactoryBean implements FactoryBean, BeanClassLoaderAware, InitializingBean { + + private static final boolean base64Present = ClassUtils.isPresent( + "org.apache.commons.codec.binary.Base64", GsonFactoryBean.class.getClassLoader()); + + private final Log logger = LogFactory.getLog(getClass()); + + private Gson gson; + + private GsonBuilder gsonBuilder; + + private Boolean prettyPrint; + + private Boolean serializeNulls; + + private Boolean disableHtmlEscaping; + + private SimpleDateFormat dateFormat; + + private Boolean base64EncodeByteArrays; + + private ClassLoader beanClassLoader; + + + /** + * Set the GsonBuilder instance to use. If not set, the GsonBuilder will be created + * using its default constructor. + */ + public void setGsonBuilder(GsonBuilder gsonBuilder) { + this.gsonBuilder = gsonBuilder; + } + + /** + * Return the GsonBuilder instance being used. + * @return the GsonBuilder instance + */ + public GsonBuilder getGsonBuilder() { + return this.gsonBuilder; + } + + /** + * Whether to use the {@link GsonBuilder#setPrettyPrinting()} when writing JSON. This + * is a shortcut for setting up a {@code Gson} as follows: + * + *
+	 * new GsonBuilder().setPrettyPrinting().create();
+	 * 
+ */ + public void setPrettyPrint(boolean prettyPrint) { + this.prettyPrint = prettyPrint; + } + + /** + * Whether to use the {@link GsonBuilder#serializeNulls()} option when writing JSON. + * This is a shortcut for setting up a {@code Gson} as follows: + * + *
+	 * new GsonBuilder().serializeNulls().create();
+	 * 
+ */ + public void setSerializeNulls(boolean serializeNulls) { + this.serializeNulls = serializeNulls; + } + + /** + * Whether to use the {@link GsonBuilder#disableHtmlEscaping()} when writing JSON. Set + * to {@code true} to disable HTML escaping in JSON. This is a shortcut for setting up + * a {@code Gson} as follows: + * + *
+	 * new GsonBuilder().disableHtmlEscaping().create();
+	 * 
+ */ + public void setDisableHtmlEscaping(boolean disableHtmlEscaping) { + this.disableHtmlEscaping = disableHtmlEscaping; + } + + /** + * Define the format for date/time with the given {@link SimpleDateFormat}. + * This is a shortcut for setting up a {@code Gson} as follows: + * + *
+	 * new GsonBuilder().setDateFormat(dateFormatPattern).create();
+	 * 
+ * + * @see #setSimpleDateFormat(String) + */ + public void setSimpleDateFormat(SimpleDateFormat dateFormat) { + this.dateFormat = dateFormat; + } + + /** + * Define the date/time format with a {@link SimpleDateFormat}. + * This is a shortcut for setting up a {@code Gson} as follows: + * + *
+	 * new GsonBuilder().setDateFormat(dateFormatPattern).create();
+	 * 
+ * + * @see #setSimpleDateFormat(SimpleDateFormat) + */ + public void setSimpleDateFormat(String format) { + this.dateFormat = new SimpleDateFormat(format); + } + + /** + * Whether to Base64 encode {@code byte[]} properties when reading and + * writing JSON. + * + *

When set to {@code true} a custom {@link com.google.gson.TypeAdapter} + * is registered via {@link GsonBuilder#registerTypeHierarchyAdapter(Class, Object)} + * that serializes a {@code byte[]} property to and from a Base64 encoded + * string instead of a JSON array. + * + *

NOTE: Use of this option requires the presence of + * Apache commons-codec on the classpath. Otherwise it is ignored. + * + * @see org.springframework.http.converter.json.GsonBase64ByteArrayJsonTypeAdapter + */ + public void setBase64EncodeByteArrays(boolean base64EncodeByteArrays) { + this.base64EncodeByteArrays = base64EncodeByteArrays; + } + + @Override + public void setBeanClassLoader(ClassLoader beanClassLoader) { + this.beanClassLoader = beanClassLoader; + } + + + @Override + public void afterPropertiesSet() throws Exception { + if (gsonBuilder == null) { + this.gsonBuilder = new GsonBuilder(); + } + if (this.prettyPrint != null && this.prettyPrint) { + this.gsonBuilder = this.gsonBuilder.setPrettyPrinting(); + } + if (this.serializeNulls != null && this.serializeNulls) { + this.gsonBuilder = this.gsonBuilder.serializeNulls(); + } + if (this.disableHtmlEscaping != null && this.disableHtmlEscaping) { + this.gsonBuilder = this.gsonBuilder.disableHtmlEscaping(); + } + if (this.dateFormat != null) { + this.gsonBuilder.setDateFormat(this.dateFormat.toPattern()); + } + if (base64Present) { + if (this.base64EncodeByteArrays != null && this.base64EncodeByteArrays) { + this.gsonBuilder.registerTypeHierarchyAdapter(byte[].class, new GsonBase64ByteArrayJsonTypeAdapter()); + } + } + else if (logger.isDebugEnabled()) { + logger.debug("org.apache.commons.codec.binary.Base64 is not available on the class path. Gson Base64 encoding is disabled."); + } + this.gson = this.gsonBuilder.create(); + } + + + /** + * Return the singleton Gson. + */ + @Override + public Gson getObject() throws Exception { + return this.gson; + } + + @Override + public Class getObjectType() { + return Gson.class; + } + + @Override + public boolean isSingleton() { + return true; + } + +} diff --git a/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java b/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java new file mode 100644 index 000000000000..0adcf4b0fe56 --- /dev/null +++ b/spring-web/src/main/java/org/springframework/http/converter/json/GsonHttpMessageConverter.java @@ -0,0 +1,216 @@ +/* + * Copyright 2002-2014 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.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Reader; +import java.lang.reflect.Type; +import java.nio.charset.Charset; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpInputMessage; +import org.springframework.http.HttpOutputMessage; +import org.springframework.http.MediaType; +import org.springframework.http.converter.AbstractHttpMessageConverter; +import org.springframework.http.converter.GenericHttpMessageConverter; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.http.converter.HttpMessageNotWritableException; +import org.springframework.util.Assert; + +import com.google.gson.Gson; +import com.google.gson.JsonIOException; +import com.google.gson.JsonParseException; +import com.google.gson.reflect.TypeToken; + +/** + * Implementation of {@link org.springframework.http.converter.HttpMessageConverter + * HttpMessageConverter} that can read and write JSON using the Google Gson library's {@link Gson} + * class. + * + *

This converter can be used to bind to typed beans or untyped + * {@link java.util.HashMap HashMap} instances. + * + *

By default this converter supports {@code application/json} and + * {@code application/*+json} but {@link #setSupportedMediaTypes + * supportedMediaTypes} can be used to change that. + * + *

Tested against Gson 2.2; compatible with Gson 2.0 and higher. + * + * @author Roy Clarkson + * @since 4.1 + */ +public class GsonHttpMessageConverter extends AbstractHttpMessageConverter + implements GenericHttpMessageConverter { + + public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); + + + private Gson gson = new Gson(); + + private String jsonPrefix; + + + /** + * Construct a new {@code GsonHttpMessageConverter}. + */ + public GsonHttpMessageConverter() { + super(new MediaType("application", "json", DEFAULT_CHARSET), + new MediaType("application", "*+json", DEFAULT_CHARSET)); + } + + /** + * Set the {@code Gson} for this view. + * If not set, a default {@link Gson#Gson() Gson} is used. + *

Setting a custom-configured {@code Gson} is one way to take further + * control of the JSON serialization process. + */ + public void setGson(Gson gson) { + Assert.notNull(gson, "Gson must not be null"); + this.gson = gson; + } + + /** + * Return the underlying {@code GsonBuilder} for this converter. + */ + public Gson getGson() { + return this.gson; + } + + /** + * Specify a custom prefix to use for JSON output. Default is none. + * + * @see #setPrefixJson + */ + public void setJsonPrefix(String jsonPrefix) { + this.jsonPrefix = jsonPrefix; + } + + /** + * Indicate whether the JSON output by this view should be prefixed with "{} &&". + * Default is {@code false}. + * + *

Prefixing the JSON string in this manner is used to help prevent JSON Hijacking. + * The prefix renders the string syntactically invalid as a script so that it cannot + * be hijacked. This prefix does not affect the evaluation of JSON, but if JSON + * validation is performed on the string, the prefix would need to be ignored. + * + * @see #setJsonPrefix + */ + public void setPrefixJson(boolean prefixJson) { + this.jsonPrefix = (prefixJson ? "{} && " : null); + } + + + @Override + public boolean canRead(Class clazz, MediaType mediaType) { + return canRead(mediaType); + } + + @Override + public boolean canRead(Type type, Class contextClass, MediaType mediaType) { + return canRead(mediaType); + } + + @Override + public boolean canWrite(Class clazz, MediaType mediaType) { + return canWrite(mediaType); + } + + @Override + protected boolean supports(Class clazz) { + // should not be called, since we override canRead/Write instead + throw new UnsupportedOperationException(); + } + + @Override + protected Object readInternal(Class clazz, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException { + + TypeToken token = getTypeToken(clazz); + return readTypeToken(token, inputMessage); + } + + @Override + public Object read(Type type, Class contextClass, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException { + + TypeToken token = getTypeToken(type); + return readTypeToken(token, inputMessage); + } + + /** + * Return the Gson {@link TypeToken} for the specified type. + *

The default implementation returns {@code TypeToken.get(type)}, but this can be + * overridden in subclasses to allow for custom generic collection handling. + * For instance: + *

+	 * protected TypeToken getTypeToken(Type type) {
+	 *   if (type instanceof Class && List.class.isAssignableFrom((Class) type)) {
+	 *     return new TypeToken>() {
+	 *     };
+	 *   } else {
+	 *     return super.getTypeToken(type);
+	 *   }
+	 * }
+	 * 
+ * @param type the type for which to return the TypeToken + * @return the type token + */ + protected TypeToken getTypeToken(Type type) { + return TypeToken.get(type); + } + + private Object readTypeToken(TypeToken token, HttpInputMessage inputMessage) throws IOException { + Reader json = new InputStreamReader(inputMessage.getBody(), getCharset(inputMessage.getHeaders())); + try { + return this.gson.fromJson(json, token.getType()); + } + catch (JsonParseException ex) { + throw new HttpMessageNotReadableException("Could not read JSON: " + ex.getMessage(), ex); + } + } + + private Charset getCharset(HttpHeaders headers) { + if (headers == null || headers.getContentType() == null || headers.getContentType().getCharSet() == null) { + return DEFAULT_CHARSET; + } + return headers.getContentType().getCharSet(); + } + + @Override + protected void writeInternal(Object o, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException { + + Charset charset = getCharset(outputMessage.getHeaders()); + OutputStreamWriter writer = new OutputStreamWriter(outputMessage.getBody(), charset); + + try { + if (this.jsonPrefix != null) { + writer.append(this.jsonPrefix); + } + gson.toJson(o, writer); + writer.close(); + } + catch(JsonIOException ex) { + throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex); + } + } + +} 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 8adb63e54520..3f3eb939ffce 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 @@ -23,6 +23,7 @@ import java.util.List; import java.util.Map; import java.util.Set; + import javax.xml.transform.Source; import org.springframework.core.ParameterizedTypeReference; @@ -42,6 +43,7 @@ import org.springframework.http.converter.StringHttpMessageConverter; 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.support.AllEncompassingFormHttpMessageConverter; import org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter; @@ -115,6 +117,7 @@ * * @author Arjen Poutsma * @author Brian Clozel + * @author Roy Clarkson * @since 3.0 * @see HttpMessageConverter * @see RequestCallback @@ -134,6 +137,8 @@ public class RestTemplate extends InterceptingHttpAccessor implements RestOperat ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", RestTemplate.class.getClassLoader()) && ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", RestTemplate.class.getClassLoader()); + private static final boolean gsonPresent = + ClassUtils.isPresent("com.google.gson.Gson", RestTemplate.class.getClassLoader()); private final List> messageConverters = new ArrayList>(); @@ -163,6 +168,9 @@ public RestTemplate() { if (jackson2Present) { this.messageConverters.add(new MappingJackson2HttpMessageConverter()); } + else if (gsonPresent) { + this.messageConverters.add(new GsonHttpMessageConverter()); + } } /** diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/GsonFactoryBeanTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/GsonFactoryBeanTests.java new file mode 100644 index 000000000000..d6650e6f8c83 --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/converter/json/GsonFactoryBeanTests.java @@ -0,0 +1,239 @@ +/* + * Copyright 2002-2014 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.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; + +import org.junit.Before; +import org.junit.Test; + +import com.google.gson.Gson; + +import static org.junit.Assert.*; + +/** + * {@link GsonFactoryBean} tests + * + * @author Roy Clarkson + */ +public class GsonFactoryBeanTests { + + private static final String NEWLINE_SYSTEM_PROPERTY = System.getProperty("line.separator"); + + private static final String DATE_FORMAT = "yyyy-MM-dd"; + + private GsonFactoryBean factory; + + + @Before + public void setUp() { + factory = new GsonFactoryBean(); + } + + @Test + public void prettyPrint() throws Exception { + this.factory.setPrettyPrint(true); + this.factory.afterPropertiesSet(); + Gson gson = this.factory.getObject(); + StringBean bean = new StringBean(); + bean.setName("Jason"); + String result = gson.toJson(bean); + assertEquals("{" + NEWLINE_SYSTEM_PROPERTY + " \"name\": \"Jason\"" + NEWLINE_SYSTEM_PROPERTY + "}", result); + } + + @Test + public void prettyPrintFalse() throws Exception { + this.factory.setPrettyPrint(false); + this.factory.afterPropertiesSet(); + Gson gson = this.factory.getObject(); + StringBean bean = new StringBean(); + bean.setName("Jason"); + String result = gson.toJson(bean); + assertEquals("{\"name\":\"Jason\"}", result); + } + + @Test + public void serializeNulls() throws Exception { + this.factory.setSerializeNulls(true); + this.factory.afterPropertiesSet(); + Gson gson = this.factory.getObject(); + StringBean bean = new StringBean(); + String result = gson.toJson(bean); + assertEquals("{\"name\":null}", result); + } + + @Test + public void serializeNullsFalse() throws Exception { + this.factory.setSerializeNulls(false); + this.factory.afterPropertiesSet(); + Gson gson = this.factory.getObject(); + StringBean bean = new StringBean(); + String result = gson.toJson(bean); + assertEquals("{}", result); + } + + @Test + public void disableHtmlEscaping() throws Exception { + this.factory.setDisableHtmlEscaping(true); + this.factory.afterPropertiesSet(); + Gson gson = this.factory.getObject(); + StringBean bean = new StringBean(); + bean.setName("Bob=Bob"); + String result = gson.toJson(bean); + assertEquals("{\"name\":\"Bob=Bob\"}", result); + } + + @Test + public void disableHtmlEscapingFalse() throws Exception { + this.factory.setDisableHtmlEscaping(false); + this.factory.afterPropertiesSet(); + Gson gson = this.factory.getObject(); + StringBean bean = new StringBean(); + bean.setName("Bob=Bob"); + String result = gson.toJson(bean); + assertEquals("{\"name\":\"Bob\\u003dBob\"}", result); + } + + @Test + public void customizeDateFormat() throws Exception { + SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT); + this.factory.setSimpleDateFormat(dateFormat); + this.factory.afterPropertiesSet(); + Gson gson = this.factory.getObject(); + DateBean bean = new DateBean(); + Calendar cal = Calendar.getInstance(); + cal.clear(); + cal.set(Calendar.YEAR, 2014); + cal.set(Calendar.MONTH, Calendar.JANUARY); + cal.set(Calendar.DATE, 1); + Date date = cal.getTime(); + bean.setDate(date); + String result = gson.toJson(bean); + assertEquals("{\"date\":\"2014-01-01\"}", result); + } + + @Test + public void customizeDateFormatString() throws Exception { + this.factory.setSimpleDateFormat(DATE_FORMAT); + this.factory.afterPropertiesSet(); + Gson gson = this.factory.getObject(); + DateBean bean = new DateBean(); + Calendar cal = Calendar.getInstance(); + cal.clear(); + cal.set(Calendar.YEAR, 2014); + cal.set(Calendar.MONTH, Calendar.JANUARY); + cal.set(Calendar.DATE, 1); + Date date = cal.getTime(); + bean.setDate(date); + String result = gson.toJson(bean); + assertEquals("{\"date\":\"2014-01-01\"}", result); + } + + @Test + public void customizeDateFormatNone() throws Exception { + this.factory.afterPropertiesSet(); + Gson gson = this.factory.getObject(); + DateBean bean = new DateBean(); + Calendar cal = Calendar.getInstance(); + cal.clear(); + cal.set(Calendar.YEAR, 2014); + cal.set(Calendar.MONTH, Calendar.JANUARY); + cal.set(Calendar.DATE, 1); + Date date = cal.getTime(); + bean.setDate(date); + String result = gson.toJson(bean); + assertEquals("{\"date\":\"Jan 1, 2014 12:00:00 AM\"}", result); + } + + @Test + public void base64EncodeByteArrays() throws Exception { + this.factory.setBase64EncodeByteArrays(true); + this.factory.afterPropertiesSet(); + Gson gson = this.factory.getObject(); + ByteArrayBean bean = new ByteArrayBean(); + bean.setBytes(new byte[] { 0x1, 0x2 }); + String result = gson.toJson(bean); + assertEquals("{\"bytes\":\"AQI\\u003d\"}", result); + } + + @Test + public void base64EncodeByteArraysDisableHtmlEscaping() throws Exception { + this.factory.setBase64EncodeByteArrays(true); + this.factory.setDisableHtmlEscaping(true); + this.factory.afterPropertiesSet(); + Gson gson = this.factory.getObject(); + ByteArrayBean bean = new ByteArrayBean(); + bean.setBytes(new byte[] { 0x1, 0x2 }); + String result = gson.toJson(bean); + assertEquals("{\"bytes\":\"AQI=\"}", result); + } + + @Test + public void base64EncodeByteArraysFalse() throws Exception { + this.factory.setBase64EncodeByteArrays(false); + this.factory.afterPropertiesSet(); + Gson gson = this.factory.getObject(); + ByteArrayBean bean = new ByteArrayBean(); + bean.setBytes(new byte[] { 0x1, 0x2 }); + String result = gson.toJson(bean); + assertEquals("{\"bytes\":[1,2]}", result); + } + + + private static class StringBean { + + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + } + + private static class DateBean { + + private Date date; + + public Date getDate() { + return this.date; + } + + public void setDate(Date date) { + this.date = date; + } + } + + public static class ByteArrayBean { + + private byte[] bytes; + + public byte[] getBytes() { + return this.bytes; + } + + public void setBytes(byte[] bytes) { + this.bytes = bytes; + } + + } + +} diff --git a/spring-web/src/test/java/org/springframework/http/converter/json/GsonHttpMessageConverterTests.java b/spring-web/src/test/java/org/springframework/http/converter/json/GsonHttpMessageConverterTests.java new file mode 100644 index 000000000000..62ba53ce605e --- /dev/null +++ b/spring-web/src/test/java/org/springframework/http/converter/json/GsonHttpMessageConverterTests.java @@ -0,0 +1,288 @@ +/* + * Copyright 2002-2014 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.lang.reflect.Type; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +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 com.google.gson.reflect.TypeToken; + +import static org.junit.Assert.*; + +/** + * Gson 2.x converter tests. + * + * @author Roy Clarkson + */ +public class GsonHttpMessageConverterTests { + + private static final Charset UTF8 = Charset.forName("UTF-8"); + + private GsonHttpMessageConverter converter = new GsonHttpMessageConverter(); + + + @Test + public void canRead() { + assertTrue(converter.canRead(MyBean.class, new MediaType("application", "json"))); + assertTrue(converter.canRead(Map.class, new MediaType("application", "json"))); + } + + @Test + public void canWrite() { + assertTrue(converter.canWrite(MyBean.class, new MediaType("application", "json"))); + assertTrue(converter.canWrite(Map.class, new MediaType("application", "json"))); + } + + @Test + public void canReadAndWriteMicroformats() { + assertTrue(converter.canRead(MyBean.class, new MediaType("application", "vnd.test-micro-type+json"))); + assertTrue(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) 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) 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}); + converter.write(body, null, outputMessage); + Charset utf8 = Charset.forName("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 { + Charset utf16 = Charset.forName("UTF-16BE"); + MediaType contentType = new MediaType("application", "json", utf16); + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + String body = "H\u00e9llo W\u00f6rld"; + converter.write(body, contentType, outputMessage); + assertEquals("Invalid result", "\"" + body + "\"", outputMessage.getBodyAsString(utf16)); + 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")); + converter.read(MyBean.class, inputMessage); + } + + @Test + @SuppressWarnings("unchecked") + public void readGenerics() throws IOException { + GsonHttpMessageConverter converter = new GsonHttpMessageConverter() { + + @Override + protected TypeToken getTypeToken(Type type) { + if (type instanceof Class && List.class.isAssignableFrom((Class) type)) { + return new TypeToken>() { + }; + } + else { + return super.getTypeToken(type); + } + } + }; + String body = "[{\"bytes\":[1,2],\"array\":[\"Foo\",\"Bar\"],\"number\":42,\"string\":\"Foo\",\"bool\":true,\"fraction\":42.0}]"; + MockHttpInputMessage inputMessage = new MockHttpInputMessage( + body.getBytes(UTF8)); + inputMessage.getHeaders().setContentType(new MediaType("application", "json")); + + List results = (List) converter.read(List.class, 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()); + } + + @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(UTF8)); + inputMessage.getHeaders().setContentType(new MediaType("application", "json")); + + GsonHttpMessageConverter converter = new GsonHttpMessageConverter(); + 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()); + } + + @Test + public void prefixJson() throws Exception { + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + this.converter.setPrefixJson(true); + this.converter.writeInternal("foo", outputMessage); + + assertEquals("{} && \"foo\"", outputMessage.getBodyAsString(UTF8)); + } + + @Test + public void prefixJsonCustom() throws Exception { + MockHttpOutputMessage outputMessage = new MockHttpOutputMessage(); + this.converter.setJsonPrefix(")]}',"); + this.converter.writeInternal("foo", outputMessage); + + assertEquals(")]}',\"foo\"", outputMessage.getBodyAsString(UTF8)); + } + + + 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; + } + } + +}