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.codec.json; 018 019import java.io.IOException; 020import java.lang.annotation.Annotation; 021import java.math.BigDecimal; 022import java.util.List; 023import java.util.Map; 024 025import com.fasterxml.jackson.core.JsonProcessingException; 026import com.fasterxml.jackson.databind.DeserializationFeature; 027import com.fasterxml.jackson.databind.JavaType; 028import com.fasterxml.jackson.databind.ObjectMapper; 029import com.fasterxml.jackson.databind.ObjectReader; 030import com.fasterxml.jackson.databind.exc.InvalidDefinitionException; 031import com.fasterxml.jackson.databind.util.TokenBuffer; 032import org.reactivestreams.Publisher; 033import reactor.core.publisher.Flux; 034import reactor.core.publisher.Mono; 035 036import org.springframework.core.MethodParameter; 037import org.springframework.core.ResolvableType; 038import org.springframework.core.codec.CodecException; 039import org.springframework.core.codec.DecodingException; 040import org.springframework.core.codec.Hints; 041import org.springframework.core.io.buffer.DataBuffer; 042import org.springframework.core.io.buffer.DataBufferLimitException; 043import org.springframework.core.io.buffer.DataBufferUtils; 044import org.springframework.core.log.LogFormatUtils; 045import org.springframework.http.codec.HttpMessageDecoder; 046import org.springframework.http.server.reactive.ServerHttpRequest; 047import org.springframework.http.server.reactive.ServerHttpResponse; 048import org.springframework.lang.Nullable; 049import org.springframework.util.Assert; 050import org.springframework.util.MimeType; 051 052/** 053 * Abstract base class for Jackson 2.9 decoding, leveraging non-blocking parsing. 054 * 055 * <p>Compatible with Jackson 2.9.7 and higher. 056 * 057 * @author Sebastien Deleuze 058 * @author Rossen Stoyanchev 059 * @author Arjen Poutsma 060 * @since 5.0 061 * @see <a href="https://github.com/FasterXML/jackson-core/issues/57" target="_blank">Add support for non-blocking ("async") JSON parsing</a> 062 */ 063public abstract class AbstractJackson2Decoder extends Jackson2CodecSupport implements HttpMessageDecoder<Object> { 064 065 private int maxInMemorySize = 256 * 1024; 066 067 068 /** 069 * Constructor with a Jackson {@link ObjectMapper} to use. 070 */ 071 protected AbstractJackson2Decoder(ObjectMapper mapper, MimeType... mimeTypes) { 072 super(mapper, mimeTypes); 073 } 074 075 076 /** 077 * Set the max number of bytes that can be buffered by this decoder. This 078 * is either the size of the entire input when decoding as a whole, or the 079 * size of one top-level JSON object within a JSON stream. When the limit 080 * is exceeded, {@link DataBufferLimitException} is raised. 081 * <p>By default this is set to 256K. 082 * @param byteCount the max number of bytes to buffer, or -1 for unlimited 083 * @since 5.1.11 084 */ 085 public void setMaxInMemorySize(int byteCount) { 086 this.maxInMemorySize = byteCount; 087 } 088 089 /** 090 * Return the {@link #setMaxInMemorySize configured} byte count limit. 091 * @since 5.1.11 092 */ 093 public int getMaxInMemorySize() { 094 return this.maxInMemorySize; 095 } 096 097 098 @Override 099 public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) { 100 JavaType javaType = getObjectMapper().constructType(elementType.getType()); 101 // Skip String: CharSequenceDecoder + "*/*" comes after 102 return (!CharSequence.class.isAssignableFrom(elementType.toClass()) && 103 getObjectMapper().canDeserialize(javaType) && supportsMimeType(mimeType)); 104 } 105 106 @Override 107 public Flux<Object> decode(Publisher<DataBuffer> input, ResolvableType elementType, 108 @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) { 109 110 ObjectMapper mapper = getObjectMapper(); 111 112 boolean forceUseOfBigDecimal = mapper.isEnabled(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS); 113 if (BigDecimal.class.equals(elementType.getType())) { 114 forceUseOfBigDecimal = true; 115 } 116 117 Flux<DataBuffer> processed = processInput(input, elementType, mimeType, hints); 118 Flux<TokenBuffer> tokens = Jackson2Tokenizer.tokenize(processed, mapper.getFactory(), mapper, 119 true, forceUseOfBigDecimal, getMaxInMemorySize()); 120 121 ObjectReader reader = getObjectReader(elementType, hints); 122 123 return tokens.handle((tokenBuffer, sink) -> { 124 try { 125 Object value = reader.readValue(tokenBuffer.asParser(getObjectMapper())); 126 logValue(value, hints); 127 if (value != null) { 128 sink.next(value); 129 } 130 } 131 catch (IOException ex) { 132 sink.error(processException(ex)); 133 } 134 }); 135 } 136 137 /** 138 * Process the input publisher into a flux. Default implementation returns 139 * {@link Flux#from(Publisher)}, but subclasses can choose to customize 140 * this behavior. 141 * @param input the {@code DataBuffer} input stream to process 142 * @param elementType the expected type of elements in the output stream 143 * @param mimeType the MIME type associated with the input stream (optional) 144 * @param hints additional information about how to do encode 145 * @return the processed flux 146 * @since 5.1.14 147 */ 148 protected Flux<DataBuffer> processInput(Publisher<DataBuffer> input, ResolvableType elementType, 149 @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) { 150 151 return Flux.from(input); 152 } 153 154 @Override 155 public Mono<Object> decodeToMono(Publisher<DataBuffer> input, ResolvableType elementType, 156 @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) { 157 158 return DataBufferUtils.join(input, this.maxInMemorySize) 159 .flatMap(dataBuffer -> Mono.justOrEmpty(decode(dataBuffer, elementType, mimeType, hints))); 160 } 161 162 @Override 163 public Object decode(DataBuffer dataBuffer, ResolvableType targetType, 164 @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) throws DecodingException { 165 166 try { 167 ObjectReader objectReader = getObjectReader(targetType, hints); 168 Object value = objectReader.readValue(dataBuffer.asInputStream()); 169 logValue(value, hints); 170 return value; 171 } 172 catch (IOException ex) { 173 throw processException(ex); 174 } 175 finally { 176 DataBufferUtils.release(dataBuffer); 177 } 178 } 179 180 private ObjectReader getObjectReader(ResolvableType elementType, @Nullable Map<String, Object> hints) { 181 Assert.notNull(elementType, "'elementType' must not be null"); 182 Class<?> contextClass = getContextClass(elementType); 183 if (contextClass == null && hints != null) { 184 contextClass = getContextClass((ResolvableType) hints.get(ACTUAL_TYPE_HINT)); 185 } 186 JavaType javaType = getJavaType(elementType.getType(), contextClass); 187 Class<?> jsonView = (hints != null ? (Class<?>) hints.get(Jackson2CodecSupport.JSON_VIEW_HINT) : null); 188 return jsonView != null ? 189 getObjectMapper().readerWithView(jsonView).forType(javaType) : 190 getObjectMapper().readerFor(javaType); 191 } 192 193 @Nullable 194 private Class<?> getContextClass(@Nullable ResolvableType elementType) { 195 MethodParameter param = (elementType != null ? getParameter(elementType) : null); 196 return (param != null ? param.getContainingClass() : null); 197 } 198 199 private void logValue(@Nullable Object value, @Nullable Map<String, Object> hints) { 200 if (!Hints.isLoggingSuppressed(hints)) { 201 LogFormatUtils.traceDebug(logger, traceOn -> { 202 String formatted = LogFormatUtils.formatValue(value, !traceOn); 203 return Hints.getLogPrefix(hints) + "Decoded [" + formatted + "]"; 204 }); 205 } 206 } 207 208 private CodecException processException(IOException ex) { 209 if (ex instanceof InvalidDefinitionException) { 210 JavaType type = ((InvalidDefinitionException) ex).getType(); 211 return new CodecException("Type definition error: " + type, ex); 212 } 213 if (ex instanceof JsonProcessingException) { 214 String originalMessage = ((JsonProcessingException) ex).getOriginalMessage(); 215 return new DecodingException("JSON decoding error: " + originalMessage, ex); 216 } 217 return new DecodingException("I/O error while parsing input stream", ex); 218 } 219 220 221 // HttpMessageDecoder 222 223 @Override 224 public Map<String, Object> getDecodeHints(ResolvableType actualType, ResolvableType elementType, 225 ServerHttpRequest request, ServerHttpResponse response) { 226 227 return getHints(actualType); 228 } 229 230 @Override 231 public List<MimeType> getDecodableMimeTypes() { 232 return getMimeTypes(); 233 } 234 235 236 // Jackson2CodecSupport 237 238 @Override 239 protected <A extends Annotation> A getAnnotation(MethodParameter parameter, Class<A> annotType) { 240 return parameter.getParameterAnnotation(annotType); 241 } 242 243}