001/* 002 * Copyright 2002-2018 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.http.converter.json; 018 019import java.io.IOException; 020import java.io.InputStreamReader; 021import java.io.OutputStreamWriter; 022import java.io.Reader; 023import java.lang.reflect.ParameterizedType; 024import java.lang.reflect.Type; 025import java.nio.charset.Charset; 026 027import com.google.gson.Gson; 028import com.google.gson.JsonIOException; 029import com.google.gson.JsonParseException; 030import com.google.gson.reflect.TypeToken; 031 032import org.springframework.http.HttpHeaders; 033import org.springframework.http.HttpInputMessage; 034import org.springframework.http.HttpOutputMessage; 035import org.springframework.http.MediaType; 036import org.springframework.http.converter.AbstractGenericHttpMessageConverter; 037import org.springframework.http.converter.HttpMessageNotReadableException; 038import org.springframework.http.converter.HttpMessageNotWritableException; 039import org.springframework.util.Assert; 040 041/** 042 * Implementation of {@link org.springframework.http.converter.HttpMessageConverter} 043 * that can read and write JSON using the 044 * <a href="https://code.google.com/p/google-gson/">Google Gson</a> library's 045 * {@link Gson} class. 046 * 047 * <p>This converter can be used to bind to typed beans or untyped {@code HashMap}s. 048 * By default, it supports {@code application/json} and {@code application/*+json} with 049 * {@code UTF-8} character set. 050 * 051 * <p>Tested against Gson 2.8; compatible with Gson 2.0 and higher. 052 * 053 * @author Roy Clarkson 054 * @since 4.1 055 * @see #setGson 056 * @see #setSupportedMediaTypes 057 */ 058public class GsonHttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> { 059 060 public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); 061 062 063 private Gson gson = new Gson(); 064 065 private String jsonPrefix; 066 067 068 /** 069 * Construct a new {@code GsonHttpMessageConverter}. 070 */ 071 public GsonHttpMessageConverter() { 072 super(MediaType.APPLICATION_JSON, new MediaType("application", "*+json")); 073 setDefaultCharset(DEFAULT_CHARSET); 074 } 075 076 077 /** 078 * Set the {@code Gson} instance to use. 079 * If not set, a default {@link Gson#Gson() Gson} instance will be used. 080 * <p>Setting a custom-configured {@code Gson} is one way to take further 081 * control of the JSON serialization process. 082 */ 083 public void setGson(Gson gson) { 084 Assert.notNull(gson, "A Gson instance is required"); 085 this.gson = gson; 086 } 087 088 /** 089 * Return the configured {@code Gson} instance for this converter. 090 */ 091 public Gson getGson() { 092 return this.gson; 093 } 094 095 /** 096 * Specify a custom prefix to use for JSON output. Default is none. 097 * @see #setPrefixJson 098 */ 099 public void setJsonPrefix(String jsonPrefix) { 100 this.jsonPrefix = jsonPrefix; 101 } 102 103 /** 104 * Indicate whether the JSON output by this view should be prefixed with ")]}', ". 105 * Default is {@code false}. 106 * <p>Prefixing the JSON string in this manner is used to help prevent JSON 107 * Hijacking. The prefix renders the string syntactically invalid as a script 108 * so that it cannot be hijacked. 109 * This prefix should be stripped before parsing the string as JSON. 110 * @see #setJsonPrefix 111 */ 112 public void setPrefixJson(boolean prefixJson) { 113 this.jsonPrefix = (prefixJson ? ")]}', " : null); 114 } 115 116 117 @Override 118 @SuppressWarnings("deprecation") 119 public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage) 120 throws IOException, HttpMessageNotReadableException { 121 122 TypeToken<?> token = getTypeToken(type); 123 return readTypeToken(token, inputMessage); 124 } 125 126 @Override 127 @SuppressWarnings("deprecation") 128 protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) 129 throws IOException, HttpMessageNotReadableException { 130 131 TypeToken<?> token = getTypeToken(clazz); 132 return readTypeToken(token, inputMessage); 133 } 134 135 /** 136 * Return the Gson {@link TypeToken} for the specified type. 137 * <p>The default implementation returns {@code TypeToken.get(type)}, but 138 * this can be overridden in subclasses to allow for custom generic 139 * collection handling. For instance: 140 * <pre class="code"> 141 * protected TypeToken<?> getTypeToken(Type type) { 142 * if (type instanceof Class && List.class.isAssignableFrom((Class<?>) type)) { 143 * return new TypeToken<ArrayList<MyBean>>() {}; 144 * } 145 * else { 146 * return super.getTypeToken(type); 147 * } 148 * } 149 * </pre> 150 * @param type the type for which to return the TypeToken 151 * @return the type token 152 * @deprecated as of Spring Framework 4.3.8, in favor of signature-based resolution 153 */ 154 @Deprecated 155 protected TypeToken<?> getTypeToken(Type type) { 156 return TypeToken.get(type); 157 } 158 159 private Object readTypeToken(TypeToken<?> token, HttpInputMessage inputMessage) throws IOException { 160 Reader json = new InputStreamReader(inputMessage.getBody(), getCharset(inputMessage.getHeaders())); 161 try { 162 return this.gson.fromJson(json, token.getType()); 163 } 164 catch (JsonParseException ex) { 165 throw new HttpMessageNotReadableException("JSON parse error: " + ex.getMessage(), ex); 166 } 167 } 168 169 private Charset getCharset(HttpHeaders headers) { 170 if (headers == null || headers.getContentType() == null || headers.getContentType().getCharset() == null) { 171 return DEFAULT_CHARSET; 172 } 173 return headers.getContentType().getCharset(); 174 } 175 176 @Override 177 protected void writeInternal(Object o, Type type, HttpOutputMessage outputMessage) 178 throws IOException, HttpMessageNotWritableException { 179 180 Charset charset = getCharset(outputMessage.getHeaders()); 181 OutputStreamWriter writer = new OutputStreamWriter(outputMessage.getBody(), charset); 182 try { 183 if (this.jsonPrefix != null) { 184 writer.append(this.jsonPrefix); 185 } 186 187 // In Gson, toJson with a type argument will exclusively use that given type, 188 // ignoring the actual type of the object... which might be more specific, 189 // e.g. a subclass of the specified type which includes additional fields. 190 // As a consequence, we're only passing in parameterized type declarations 191 // which might contain extra generics that the object instance doesn't retain. 192 if (type instanceof ParameterizedType) { 193 this.gson.toJson(o, type, writer); 194 } 195 else { 196 this.gson.toJson(o, writer); 197 } 198 199 writer.flush(); 200 } 201 catch (JsonIOException ex) { 202 throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex); 203 } 204 } 205 206}