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.lang.reflect.ParameterizedType; 021import java.lang.reflect.Type; 022import java.lang.reflect.TypeVariable; 023import java.nio.charset.Charset; 024import java.util.concurrent.atomic.AtomicReference; 025 026import com.fasterxml.jackson.core.JsonEncoding; 027import com.fasterxml.jackson.core.JsonGenerator; 028import com.fasterxml.jackson.core.JsonProcessingException; 029import com.fasterxml.jackson.core.PrettyPrinter; 030import com.fasterxml.jackson.core.util.DefaultIndenter; 031import com.fasterxml.jackson.core.util.DefaultPrettyPrinter; 032import com.fasterxml.jackson.databind.JavaType; 033import com.fasterxml.jackson.databind.JsonMappingException; 034import com.fasterxml.jackson.databind.ObjectMapper; 035import com.fasterxml.jackson.databind.ObjectWriter; 036import com.fasterxml.jackson.databind.SerializationConfig; 037import com.fasterxml.jackson.databind.SerializationFeature; 038import com.fasterxml.jackson.databind.ser.FilterProvider; 039import com.fasterxml.jackson.databind.type.TypeFactory; 040 041import org.springframework.core.ResolvableType; 042import org.springframework.http.HttpInputMessage; 043import org.springframework.http.HttpOutputMessage; 044import org.springframework.http.MediaType; 045import org.springframework.http.converter.AbstractGenericHttpMessageConverter; 046import org.springframework.http.converter.HttpMessageConverter; 047import org.springframework.http.converter.HttpMessageNotReadableException; 048import org.springframework.http.converter.HttpMessageNotWritableException; 049import org.springframework.util.Assert; 050import org.springframework.util.TypeUtils; 051 052/** 053 * Abstract base class for Jackson based and content type independent 054 * {@link HttpMessageConverter} implementations. 055 * 056 * <p>Compatible with Jackson 2.6 and higher, as of Spring 4.3. 057 * 058 * @author Arjen Poutsma 059 * @author Keith Donald 060 * @author Rossen Stoyanchev 061 * @author Juergen Hoeller 062 * @author Sebastien Deleuze 063 * @since 4.1 064 * @see MappingJackson2HttpMessageConverter 065 */ 066public abstract class AbstractJackson2HttpMessageConverter extends AbstractGenericHttpMessageConverter<Object> { 067 068 public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); 069 070 private static final MediaType TEXT_EVENT_STREAM = new MediaType("text", "event-stream"); 071 072 073 protected ObjectMapper objectMapper; 074 075 private Boolean prettyPrint; 076 077 private PrettyPrinter ssePrettyPrinter; 078 079 080 protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper) { 081 init(objectMapper); 082 } 083 084 protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper, MediaType supportedMediaType) { 085 super(supportedMediaType); 086 init(objectMapper); 087 } 088 089 protected AbstractJackson2HttpMessageConverter(ObjectMapper objectMapper, MediaType... supportedMediaTypes) { 090 super(supportedMediaTypes); 091 init(objectMapper); 092 } 093 094 protected void init(ObjectMapper objectMapper) { 095 this.objectMapper = objectMapper; 096 setDefaultCharset(DEFAULT_CHARSET); 097 DefaultPrettyPrinter prettyPrinter = new DefaultPrettyPrinter(); 098 prettyPrinter.indentObjectsWith(new DefaultIndenter(" ", "\ndata:")); 099 this.ssePrettyPrinter = prettyPrinter; 100 } 101 102 103 /** 104 * Set the {@code ObjectMapper} for this view. 105 * If not set, a default {@link ObjectMapper#ObjectMapper() ObjectMapper} is used. 106 * <p>Setting a custom-configured {@code ObjectMapper} is one way to take further 107 * control of the JSON serialization process. For example, an extended 108 * {@link com.fasterxml.jackson.databind.ser.SerializerFactory} 109 * can be configured that provides custom serializers for specific types. 110 * The other option for refining the serialization process is to use Jackson's 111 * provided annotations on the types to be serialized, in which case a 112 * custom-configured ObjectMapper is unnecessary. 113 */ 114 public void setObjectMapper(ObjectMapper objectMapper) { 115 Assert.notNull(objectMapper, "ObjectMapper must not be null"); 116 this.objectMapper = objectMapper; 117 configurePrettyPrint(); 118 } 119 120 /** 121 * Return the underlying {@code ObjectMapper} for this view. 122 */ 123 public ObjectMapper getObjectMapper() { 124 return this.objectMapper; 125 } 126 127 /** 128 * Whether to use the {@link DefaultPrettyPrinter} when writing JSON. 129 * This is a shortcut for setting up an {@code ObjectMapper} as follows: 130 * <pre class="code"> 131 * ObjectMapper mapper = new ObjectMapper(); 132 * mapper.configure(SerializationFeature.INDENT_OUTPUT, true); 133 * converter.setObjectMapper(mapper); 134 * </pre> 135 */ 136 public void setPrettyPrint(boolean prettyPrint) { 137 this.prettyPrint = prettyPrint; 138 configurePrettyPrint(); 139 } 140 141 private void configurePrettyPrint() { 142 if (this.prettyPrint != null) { 143 this.objectMapper.configure(SerializationFeature.INDENT_OUTPUT, this.prettyPrint); 144 } 145 } 146 147 148 @Override 149 public boolean canRead(Class<?> clazz, MediaType mediaType) { 150 return canRead(clazz, null, mediaType); 151 } 152 153 @Override 154 public boolean canRead(Type type, Class<?> contextClass, MediaType mediaType) { 155 if (!canRead(mediaType)) { 156 return false; 157 } 158 JavaType javaType = getJavaType(type, contextClass); 159 AtomicReference<Throwable> causeRef = new AtomicReference<Throwable>(); 160 if (this.objectMapper.canDeserialize(javaType, causeRef)) { 161 return true; 162 } 163 logWarningIfNecessary(javaType, causeRef.get()); 164 return false; 165 } 166 167 @Override 168 public boolean canWrite(Class<?> clazz, MediaType mediaType) { 169 if (!canWrite(mediaType)) { 170 return false; 171 } 172 AtomicReference<Throwable> causeRef = new AtomicReference<Throwable>(); 173 if (this.objectMapper.canSerialize(clazz, causeRef)) { 174 return true; 175 } 176 logWarningIfNecessary(clazz, causeRef.get()); 177 return false; 178 } 179 180 /** 181 * Determine whether to log the given exception coming from a 182 * {@link ObjectMapper#canDeserialize} / {@link ObjectMapper#canSerialize} check. 183 * @param type the class that Jackson tested for (de-)serializability 184 * @param cause the Jackson-thrown exception to evaluate 185 * (typically a {@link JsonMappingException}) 186 * @since 4.3 187 */ 188 protected void logWarningIfNecessary(Type type, Throwable cause) { 189 if (cause == null) { 190 return; 191 } 192 193 // Do not log warning for serializer not found (note: different message wording on Jackson 2.9) 194 boolean debugLevel = (cause instanceof JsonMappingException && 195 (cause.getMessage().startsWith("Can not find") || cause.getMessage().startsWith("Cannot find"))); 196 197 if (debugLevel ? logger.isDebugEnabled() : logger.isWarnEnabled()) { 198 String msg = "Failed to evaluate Jackson " + (type instanceof JavaType ? "de" : "") + 199 "serialization for type [" + type + "]"; 200 if (debugLevel) { 201 logger.debug(msg, cause); 202 } 203 else if (logger.isDebugEnabled()) { 204 logger.warn(msg, cause); 205 } 206 else { 207 logger.warn(msg + ": " + cause); 208 } 209 } 210 } 211 212 @Override 213 protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) 214 throws IOException, HttpMessageNotReadableException { 215 216 JavaType javaType = getJavaType(clazz, null); 217 return readJavaType(javaType, inputMessage); 218 } 219 220 @Override 221 public Object read(Type type, Class<?> contextClass, HttpInputMessage inputMessage) 222 throws IOException, HttpMessageNotReadableException { 223 224 JavaType javaType = getJavaType(type, contextClass); 225 return readJavaType(javaType, inputMessage); 226 } 227 228 private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) { 229 try { 230 if (inputMessage instanceof MappingJacksonInputMessage) { 231 Class<?> deserializationView = ((MappingJacksonInputMessage) inputMessage).getDeserializationView(); 232 if (deserializationView != null) { 233 return this.objectMapper.readerWithView(deserializationView).forType(javaType). 234 readValue(inputMessage.getBody()); 235 } 236 } 237 return this.objectMapper.readValue(inputMessage.getBody(), javaType); 238 } 239 catch (JsonProcessingException ex) { 240 throw new HttpMessageNotReadableException("JSON parse error: " + ex.getOriginalMessage(), ex); 241 } 242 catch (IOException ex) { 243 throw new HttpMessageNotReadableException("I/O error while reading input message", ex); 244 } 245 } 246 247 @Override 248 protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) 249 throws IOException, HttpMessageNotWritableException { 250 251 MediaType contentType = outputMessage.getHeaders().getContentType(); 252 JsonEncoding encoding = getJsonEncoding(contentType); 253 JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding); 254 try { 255 writePrefix(generator, object); 256 257 Object value = object; 258 Class<?> serializationView = null; 259 FilterProvider filters = null; 260 JavaType javaType = null; 261 262 if (object instanceof MappingJacksonValue) { 263 MappingJacksonValue container = (MappingJacksonValue) object; 264 value = container.getValue(); 265 serializationView = container.getSerializationView(); 266 filters = container.getFilters(); 267 } 268 if (type != null && value != null && TypeUtils.isAssignable(type, value.getClass())) { 269 javaType = getJavaType(type, null); 270 } 271 272 ObjectWriter objectWriter = (serializationView != null ? 273 this.objectMapper.writerWithView(serializationView) : this.objectMapper.writer()); 274 if (filters != null) { 275 objectWriter = objectWriter.with(filters); 276 } 277 if (javaType != null && javaType.isContainerType()) { 278 objectWriter = objectWriter.forType(javaType); 279 } 280 SerializationConfig config = objectWriter.getConfig(); 281 if (contentType != null && contentType.isCompatibleWith(TEXT_EVENT_STREAM) && 282 config.isEnabled(SerializationFeature.INDENT_OUTPUT)) { 283 objectWriter = objectWriter.with(this.ssePrettyPrinter); 284 } 285 objectWriter.writeValue(generator, value); 286 287 writeSuffix(generator, object); 288 generator.flush(); 289 } 290 catch (JsonProcessingException ex) { 291 throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getOriginalMessage(), ex); 292 } 293 } 294 295 /** 296 * Write a prefix before the main content. 297 * @param generator the generator to use for writing content. 298 * @param object the object to write to the output message. 299 */ 300 protected void writePrefix(JsonGenerator generator, Object object) throws IOException { 301 } 302 303 /** 304 * Write a suffix after the main content. 305 * @param generator the generator to use for writing content. 306 * @param object the object to write to the output message. 307 */ 308 protected void writeSuffix(JsonGenerator generator, Object object) throws IOException { 309 } 310 311 /** 312 * Return the Jackson {@link JavaType} for the specified type and context class. 313 * <p>The default implementation returns {@code typeFactory.constructType(type, contextClass)}, 314 * but this can be overridden in subclasses, to allow for custom generic collection handling. 315 * For instance: 316 * <pre class="code"> 317 * protected JavaType getJavaType(Type type) { 318 * if (type instanceof Class && List.class.isAssignableFrom((Class)type)) { 319 * return TypeFactory.collectionType(ArrayList.class, MyBean.class); 320 * } else { 321 * return super.getJavaType(type); 322 * } 323 * } 324 * </pre> 325 * @param type the generic type to return the Jackson JavaType for 326 * @param contextClass a context class for the target type, for example a class 327 * in which the target type appears in a method signature (can be {@code null}) 328 * @return the Jackson JavaType 329 */ 330 protected JavaType getJavaType(Type type, Class<?> contextClass) { 331 TypeFactory typeFactory = this.objectMapper.getTypeFactory(); 332 if (contextClass != null) { 333 ResolvableType resolvedType = ResolvableType.forType(type); 334 if (type instanceof TypeVariable) { 335 ResolvableType resolvedTypeVariable = resolveVariable( 336 (TypeVariable<?>) type, ResolvableType.forClass(contextClass)); 337 if (resolvedTypeVariable != ResolvableType.NONE) { 338 return typeFactory.constructType(resolvedTypeVariable.resolve()); 339 } 340 } 341 else if (type instanceof ParameterizedType && resolvedType.hasUnresolvableGenerics()) { 342 ParameterizedType parameterizedType = (ParameterizedType) type; 343 Class<?>[] generics = new Class<?>[parameterizedType.getActualTypeArguments().length]; 344 Type[] typeArguments = parameterizedType.getActualTypeArguments(); 345 for (int i = 0; i < typeArguments.length; i++) { 346 Type typeArgument = typeArguments[i]; 347 if (typeArgument instanceof TypeVariable) { 348 ResolvableType resolvedTypeArgument = resolveVariable( 349 (TypeVariable<?>) typeArgument, ResolvableType.forClass(contextClass)); 350 if (resolvedTypeArgument != ResolvableType.NONE) { 351 generics[i] = resolvedTypeArgument.resolve(); 352 } 353 else { 354 generics[i] = ResolvableType.forType(typeArgument).resolve(); 355 } 356 } 357 else { 358 generics[i] = ResolvableType.forType(typeArgument).resolve(); 359 } 360 } 361 return typeFactory.constructType(ResolvableType. 362 forClassWithGenerics(resolvedType.getRawClass(), generics).getType()); 363 } 364 } 365 return typeFactory.constructType(type); 366 } 367 368 private ResolvableType resolveVariable(TypeVariable<?> typeVariable, ResolvableType contextType) { 369 ResolvableType resolvedType; 370 if (contextType.hasGenerics()) { 371 resolvedType = ResolvableType.forType(typeVariable, contextType); 372 if (resolvedType.resolve() != null) { 373 return resolvedType; 374 } 375 } 376 377 ResolvableType superType = contextType.getSuperType(); 378 if (superType != ResolvableType.NONE) { 379 resolvedType = resolveVariable(typeVariable, superType); 380 if (resolvedType.resolve() != null) { 381 return resolvedType; 382 } 383 } 384 for (ResolvableType ifc : contextType.getInterfaces()) { 385 resolvedType = resolveVariable(typeVariable, ifc); 386 if (resolvedType.resolve() != null) { 387 return resolvedType; 388 } 389 } 390 return ResolvableType.NONE; 391 } 392 393 /** 394 * Determine the JSON encoding to use for the given content type. 395 * @param contentType the media type as requested by the caller 396 * @return the JSON encoding to use (never {@code null}) 397 */ 398 protected JsonEncoding getJsonEncoding(MediaType contentType) { 399 if (contentType != null && contentType.getCharset() != null) { 400 Charset charset = contentType.getCharset(); 401 for (JsonEncoding encoding : JsonEncoding.values()) { 402 if (charset.name().equals(encoding.getJavaName())) { 403 return encoding; 404 } 405 } 406 } 407 return JsonEncoding.UTF8; 408 } 409 410 @Override 411 protected MediaType getDefaultContentType(Object object) throws IOException { 412 if (object instanceof MappingJacksonValue) { 413 object = ((MappingJacksonValue) object).getValue(); 414 } 415 return super.getDefaultContentType(object); 416 } 417 418 @Override 419 protected Long getContentLength(Object object, MediaType contentType) throws IOException { 420 if (object instanceof MappingJacksonValue) { 421 object = ((MappingJacksonValue) object).getValue(); 422 } 423 return super.getContentLength(object, contentType); 424 } 425 426}