1 /* 2 * Copyright 2002-2011 the original author or authors. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package org.springframework.http.converter.json; 18 19 import java.io.IOException; 20 import java.nio.charset.Charset; 21 import java.util.List; 22 23 import org.codehaus.jackson.JsonEncoding; 24 import org.codehaus.jackson.JsonGenerator; 25 import org.codehaus.jackson.map.ObjectMapper; 26 import org.codehaus.jackson.map.type.TypeFactory; 27 import org.codehaus.jackson.type.JavaType; 28 import org.springframework.http.HttpInputMessage; 29 import org.springframework.http.HttpOutputMessage; 30 import org.springframework.http.MediaType; 31 import org.springframework.http.converter.AbstractHttpMessageConverter; 32 import org.springframework.http.converter.HttpMessageNotReadableException; 33 import org.springframework.http.converter.HttpMessageNotWritableException; 34 import org.springframework.util.Assert; 35 36 /** 37 * Implementation of {@link org.springframework.http.converter.HttpMessageConverter HttpMessageConverter} 38 * that can read and write JSON using <a href="http://jackson.codehaus.org/">Jackson's</a> {@link ObjectMapper}. 39 * 40 * <p>This converter can be used to bind to typed beans, or untyped {@link java.util.HashMap HashMap} instances. 41 * 42 * <p>By default, this converter supports {@code application/json}. This can be overridden by setting the 43 * {@link #setSupportedMediaTypes(List) supportedMediaTypes} property. 44 * 45 * @author Arjen Poutsma 46 * @since 3.0 47 * @see org.springframework.web.servlet.view.json.MappingJacksonJsonView 48 */ 49 public class MappingJacksonHttpMessageConverter extends AbstractHttpMessageConverter<Object> { 50 51 public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); 52 53 54 private ObjectMapper objectMapper = new ObjectMapper(); 55 56 private boolean prefixJson = false; 57 58 59 /** 60 * Construct a new {@code BindingJacksonHttpMessageConverter}. 61 */ MappingJacksonHttpMessageConverter()62 public MappingJacksonHttpMessageConverter() { 63 super(new MediaType("application", "json", DEFAULT_CHARSET)); 64 } 65 66 /** 67 * Set the {@code ObjectMapper} for this view. If not set, a default 68 * {@link ObjectMapper#ObjectMapper() ObjectMapper} is used. 69 * <p>Setting a custom-configured {@code ObjectMapper} is one way to take further control of the JSON 70 * serialization process. For example, an extended {@link org.codehaus.jackson.map.SerializerFactory} 71 * can be configured that provides custom serializers for specific types. The other option for refining 72 * the serialization process is to use Jackson's provided annotations on the types to be serialized, 73 * in which case a custom-configured ObjectMapper is unnecessary. 74 */ setObjectMapper(ObjectMapper objectMapper)75 public void setObjectMapper(ObjectMapper objectMapper) { 76 Assert.notNull(objectMapper, "ObjectMapper must not be null"); 77 this.objectMapper = objectMapper; 78 } 79 80 /** 81 * Return the underlying {@code ObjectMapper} for this view. 82 */ getObjectMapper()83 public ObjectMapper getObjectMapper() { 84 return this.objectMapper; 85 } 86 87 /** 88 * Indicate whether the JSON output by this view should be prefixed with "{} &&". Default is false. 89 * <p>Prefixing the JSON string in this manner is used to help prevent JSON Hijacking. 90 * The prefix renders the string syntactically invalid as a script so that it cannot be hijacked. 91 * This prefix does not affect the evaluation of JSON, but if JSON validation is performed on the 92 * string, the prefix would need to be ignored. 93 */ setPrefixJson(boolean prefixJson)94 public void setPrefixJson(boolean prefixJson) { 95 this.prefixJson = prefixJson; 96 } 97 98 99 @Override canRead(Class<?> clazz, MediaType mediaType)100 public boolean canRead(Class<?> clazz, MediaType mediaType) { 101 JavaType javaType = getJavaType(clazz); 102 return (this.objectMapper.canDeserialize(javaType) && canRead(mediaType)); 103 } 104 105 @Override canWrite(Class<?> clazz, MediaType mediaType)106 public boolean canWrite(Class<?> clazz, MediaType mediaType) { 107 return (this.objectMapper.canSerialize(clazz) && canWrite(mediaType)); 108 } 109 110 @Override supports(Class<?> clazz)111 protected boolean supports(Class<?> clazz) { 112 // should not be called, since we override canRead/Write instead 113 throw new UnsupportedOperationException(); 114 } 115 116 @Override readInternal(Class<?> clazz, HttpInputMessage inputMessage)117 protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) 118 throws IOException, HttpMessageNotReadableException { 119 120 JavaType javaType = getJavaType(clazz); 121 try { 122 return this.objectMapper.readValue(inputMessage.getBody(), javaType); 123 } 124 catch (IOException ex) { 125 throw new HttpMessageNotReadableException("Could not read JSON: " + ex.getMessage(), ex); 126 } 127 } 128 129 @Override writeInternal(Object object, HttpOutputMessage outputMessage)130 protected void writeInternal(Object object, HttpOutputMessage outputMessage) 131 throws IOException, HttpMessageNotWritableException { 132 133 JsonEncoding encoding = getJsonEncoding(outputMessage.getHeaders().getContentType()); 134 JsonGenerator jsonGenerator = 135 this.objectMapper.getJsonFactory().createJsonGenerator(outputMessage.getBody(), encoding); 136 try { 137 if (this.prefixJson) { 138 jsonGenerator.writeRaw("{} && "); 139 } 140 this.objectMapper.writeValue(jsonGenerator, object); 141 } 142 catch (IOException ex) { 143 throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getMessage(), ex); 144 } 145 } 146 147 148 /** 149 * Return the Jackson {@link JavaType} for the specified class. 150 * <p>The default implementation returns {@link TypeFactory#type(java.lang.reflect.Type)}, 151 * but this can be overridden in subclasses, to allow for custom generic collection handling. 152 * For instance: 153 * <pre class="code"> 154 * protected JavaType getJavaType(Class<?> clazz) { 155 * if (List.class.isAssignableFrom(clazz)) { 156 * return TypeFactory.collectionType(ArrayList.class, MyBean.class); 157 * } else { 158 * return super.getJavaType(clazz); 159 * } 160 * } 161 * </pre> 162 * @param clazz the class to return the java type for 163 * @return the java type 164 */ getJavaType(Class<?> clazz)165 protected JavaType getJavaType(Class<?> clazz) { 166 return TypeFactory.type(clazz); 167 } 168 169 /** 170 * Determine the JSON encoding to use for the given content type. 171 * @param contentType the media type as requested by the caller 172 * @return the JSON encoding to use (never <code>null</code>) 173 */ getJsonEncoding(MediaType contentType)174 protected JsonEncoding getJsonEncoding(MediaType contentType) { 175 if (contentType != null && contentType.getCharSet() != null) { 176 Charset charset = contentType.getCharSet(); 177 for (JsonEncoding encoding : JsonEncoding.values()) { 178 if (charset.name().equals(encoding.getJavaName())) { 179 return encoding; 180 } 181 } 182 } 183 return JsonEncoding.UTF8; 184 } 185 186 } 187