001/* 002 * Copyright 2002-2019 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.web.reactive.result.method.annotation; 018 019import java.lang.annotation.Annotation; 020import java.util.EnumSet; 021import java.util.List; 022import java.util.Map; 023import java.util.Set; 024import java.util.stream.Collectors; 025 026import reactor.core.publisher.Flux; 027import reactor.core.publisher.Mono; 028 029import org.springframework.core.Conventions; 030import org.springframework.core.MethodParameter; 031import org.springframework.core.ReactiveAdapter; 032import org.springframework.core.ReactiveAdapterRegistry; 033import org.springframework.core.ResolvableType; 034import org.springframework.core.annotation.AnnotationUtils; 035import org.springframework.core.codec.DecodingException; 036import org.springframework.core.codec.Hints; 037import org.springframework.core.io.buffer.DataBuffer; 038import org.springframework.core.io.buffer.DataBufferUtils; 039import org.springframework.http.HttpMethod; 040import org.springframework.http.MediaType; 041import org.springframework.http.codec.HttpMessageReader; 042import org.springframework.http.server.reactive.ServerHttpRequest; 043import org.springframework.http.server.reactive.ServerHttpResponse; 044import org.springframework.lang.Nullable; 045import org.springframework.util.Assert; 046import org.springframework.validation.Validator; 047import org.springframework.validation.annotation.Validated; 048import org.springframework.web.bind.support.WebExchangeBindException; 049import org.springframework.web.bind.support.WebExchangeDataBinder; 050import org.springframework.web.reactive.BindingContext; 051import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolverSupport; 052import org.springframework.web.server.ServerWebExchange; 053import org.springframework.web.server.ServerWebInputException; 054import org.springframework.web.server.UnsupportedMediaTypeStatusException; 055 056/** 057 * Abstract base class for argument resolvers that resolve method arguments 058 * by reading the request body with an {@link HttpMessageReader}. 059 * 060 * <p>Applies validation if the method argument is annotated with 061 * {@code @javax.validation.Valid} or 062 * {@link org.springframework.validation.annotation.Validated}. Validation 063 * failure results in an {@link ServerWebInputException}. 064 * 065 * @author Rossen Stoyanchev 066 * @author Sebastien Deleuze 067 * @since 5.0 068 */ 069public abstract class AbstractMessageReaderArgumentResolver extends HandlerMethodArgumentResolverSupport { 070 071 private static final Set<HttpMethod> SUPPORTED_METHODS = 072 EnumSet.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH); 073 074 075 private final List<HttpMessageReader<?>> messageReaders; 076 077 private final List<MediaType> supportedMediaTypes; 078 079 080 /** 081 * Constructor with {@link HttpMessageReader}'s and a {@link Validator}. 082 * @param readers the readers to convert from the request body 083 */ 084 protected AbstractMessageReaderArgumentResolver(List<HttpMessageReader<?>> readers) { 085 this(readers, ReactiveAdapterRegistry.getSharedInstance()); 086 } 087 088 /** 089 * Constructor that also accepts a {@link ReactiveAdapterRegistry}. 090 * @param messageReaders readers to convert from the request body 091 * @param adapterRegistry for adapting to other reactive types from Flux and Mono 092 */ 093 protected AbstractMessageReaderArgumentResolver( 094 List<HttpMessageReader<?>> messageReaders, ReactiveAdapterRegistry adapterRegistry) { 095 096 super(adapterRegistry); 097 Assert.notEmpty(messageReaders, "At least one HttpMessageReader is required"); 098 Assert.notNull(adapterRegistry, "ReactiveAdapterRegistry is required"); 099 this.messageReaders = messageReaders; 100 this.supportedMediaTypes = messageReaders.stream() 101 .flatMap(converter -> converter.getReadableMediaTypes().stream()) 102 .collect(Collectors.toList()); 103 } 104 105 106 /** 107 * Return the configured message converters. 108 */ 109 public List<HttpMessageReader<?>> getMessageReaders() { 110 return this.messageReaders; 111 } 112 113 114 /** 115 * Read the body from a method argument with {@link HttpMessageReader}. 116 * @param bodyParameter the {@link MethodParameter} to read 117 * @param isBodyRequired true if the body is required 118 * @param bindingContext the binding context to use 119 * @param exchange the current exchange 120 * @return the body 121 * @see #readBody(MethodParameter, MethodParameter, boolean, BindingContext, ServerWebExchange) 122 */ 123 protected Mono<Object> readBody(MethodParameter bodyParameter, boolean isBodyRequired, 124 BindingContext bindingContext, ServerWebExchange exchange) { 125 126 return this.readBody(bodyParameter, null, isBodyRequired, bindingContext, exchange); 127 } 128 129 /** 130 * Read the body from a method argument with {@link HttpMessageReader}. 131 * @param bodyParam represents the element type for the body 132 * @param actualParam the actual method argument type; possibly different 133 * from {@code bodyParam}, e.g. for an {@code HttpEntity} argument 134 * @param isBodyRequired true if the body is required 135 * @param bindingContext the binding context to use 136 * @param exchange the current exchange 137 * @return a Mono with the value to use for the method argument 138 * @since 5.0.2 139 */ 140 protected Mono<Object> readBody(MethodParameter bodyParam, @Nullable MethodParameter actualParam, 141 boolean isBodyRequired, BindingContext bindingContext, ServerWebExchange exchange) { 142 143 ResolvableType bodyType = ResolvableType.forMethodParameter(bodyParam); 144 ResolvableType actualType = (actualParam != null ? ResolvableType.forMethodParameter(actualParam) : bodyType); 145 Class<?> resolvedType = bodyType.resolve(); 146 ReactiveAdapter adapter = (resolvedType != null ? getAdapterRegistry().getAdapter(resolvedType) : null); 147 ResolvableType elementType = (adapter != null ? bodyType.getGeneric() : bodyType); 148 isBodyRequired = isBodyRequired || (adapter != null && !adapter.supportsEmpty()); 149 150 ServerHttpRequest request = exchange.getRequest(); 151 ServerHttpResponse response = exchange.getResponse(); 152 153 MediaType contentType = request.getHeaders().getContentType(); 154 MediaType mediaType = (contentType != null ? contentType : MediaType.APPLICATION_OCTET_STREAM); 155 Object[] hints = extractValidationHints(bodyParam); 156 157 if (mediaType.isCompatibleWith(MediaType.APPLICATION_FORM_URLENCODED)) { 158 return Mono.error(new IllegalStateException( 159 "In a WebFlux application, form data is accessed via ServerWebExchange.getFormData().")); 160 } 161 162 if (logger.isDebugEnabled()) { 163 logger.debug(exchange.getLogPrefix() + (contentType != null ? 164 "Content-Type:" + contentType : 165 "No Content-Type, using " + MediaType.APPLICATION_OCTET_STREAM)); 166 } 167 168 for (HttpMessageReader<?> reader : getMessageReaders()) { 169 if (reader.canRead(elementType, mediaType)) { 170 Map<String, Object> readHints = Hints.from(Hints.LOG_PREFIX_HINT, exchange.getLogPrefix()); 171 if (adapter != null && adapter.isMultiValue()) { 172 if (logger.isDebugEnabled()) { 173 logger.debug(exchange.getLogPrefix() + "0..N [" + elementType + "]"); 174 } 175 Flux<?> flux = reader.read(actualType, elementType, request, response, readHints); 176 flux = flux.onErrorResume(ex -> Flux.error(handleReadError(bodyParam, ex))); 177 if (isBodyRequired) { 178 flux = flux.switchIfEmpty(Flux.error(() -> handleMissingBody(bodyParam))); 179 } 180 if (hints != null) { 181 flux = flux.doOnNext(target -> 182 validate(target, hints, bodyParam, bindingContext, exchange)); 183 } 184 return Mono.just(adapter.fromPublisher(flux)); 185 } 186 else { 187 // Single-value (with or without reactive type wrapper) 188 if (logger.isDebugEnabled()) { 189 logger.debug(exchange.getLogPrefix() + "0..1 [" + elementType + "]"); 190 } 191 Mono<?> mono = reader.readMono(actualType, elementType, request, response, readHints); 192 mono = mono.onErrorResume(ex -> Mono.error(handleReadError(bodyParam, ex))); 193 if (isBodyRequired) { 194 mono = mono.switchIfEmpty(Mono.error(() -> handleMissingBody(bodyParam))); 195 } 196 if (hints != null) { 197 mono = mono.doOnNext(target -> 198 validate(target, hints, bodyParam, bindingContext, exchange)); 199 } 200 return (adapter != null ? Mono.just(adapter.fromPublisher(mono)) : Mono.from(mono)); 201 } 202 } 203 } 204 205 // No compatible reader but body may be empty.. 206 207 HttpMethod method = request.getMethod(); 208 if (contentType == null && method != null && SUPPORTED_METHODS.contains(method)) { 209 Flux<DataBuffer> body = request.getBody().doOnNext(buffer -> { 210 DataBufferUtils.release(buffer); 211 // Body not empty, back to 415.. 212 throw new UnsupportedMediaTypeStatusException(mediaType, this.supportedMediaTypes, elementType); 213 }); 214 if (isBodyRequired) { 215 body = body.switchIfEmpty(Mono.error(() -> handleMissingBody(bodyParam))); 216 } 217 return (adapter != null ? Mono.just(adapter.fromPublisher(body)) : Mono.from(body)); 218 } 219 220 return Mono.error(new UnsupportedMediaTypeStatusException(mediaType, this.supportedMediaTypes, elementType)); 221 } 222 223 private Throwable handleReadError(MethodParameter parameter, Throwable ex) { 224 return (ex instanceof DecodingException ? 225 new ServerWebInputException("Failed to read HTTP message", parameter, ex) : ex); 226 } 227 228 private ServerWebInputException handleMissingBody(MethodParameter parameter) { 229 String paramInfo = parameter.getExecutable().toGenericString(); 230 return new ServerWebInputException("Request body is missing: " + paramInfo, parameter); 231 } 232 233 /** 234 * Check if the given MethodParameter requires validation and if so return 235 * a (possibly empty) Object[] with validation hints. A return value of 236 * {@code null} indicates that validation is not required. 237 */ 238 @Nullable 239 private Object[] extractValidationHints(MethodParameter parameter) { 240 Annotation[] annotations = parameter.getParameterAnnotations(); 241 for (Annotation ann : annotations) { 242 Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class); 243 if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) { 244 Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann)); 245 return (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); 246 } 247 } 248 return null; 249 } 250 251 private void validate(Object target, Object[] validationHints, MethodParameter param, 252 BindingContext binding, ServerWebExchange exchange) { 253 254 String name = Conventions.getVariableNameForParameter(param); 255 WebExchangeDataBinder binder = binding.createDataBinder(exchange, target, name); 256 binder.validate(validationHints); 257 if (binder.getBindingResult().hasErrors()) { 258 throw new WebExchangeBindException(param, binder.getBindingResult()); 259 } 260 } 261 262}