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