/* * Copyright 2002-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.springframework.web.reactive.result.method.annotation; import java.lang.annotation.Annotation; import java.util.List; import java.util.function.Function; import java.util.stream.Collectors; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.core.Conventions; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.http.MediaType; import org.springframework.http.converter.reactive.HttpMessageConverter; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.validation.BeanPropertyBindingResult; import org.springframework.validation.Errors; import org.springframework.validation.SmartValidator; import org.springframework.validation.Validator; import org.springframework.validation.annotation.Validated; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebInputException; import org.springframework.web.server.UnsupportedMediaTypeStatusException; /** * Abstract base class for argument resolvers that resolve method arguments * by reading the request body with an {@link HttpMessageConverter}. * * <p>Applies validation if the method argument is annotated with * {@code @javax.validation.Valid} or * {@link org.springframework.validation.annotation.Validated}. Validation * failure results in an {@link ServerWebInputException}. * * @author Rossen Stoyanchev */ public abstract class AbstractMessageConverterArgumentResolver { private static final TypeDescriptor MONO_TYPE = TypeDescriptor.valueOf(Mono.class); private static final TypeDescriptor FLUX_TYPE = TypeDescriptor.valueOf(Flux.class); private final List<HttpMessageConverter<?>> messageConverters; private final ConversionService conversionService; private final Validator validator; private final List<MediaType> supportedMediaTypes; /** * Constructor with message converters and a ConversionService. * @param converters converters for reading the request body with * @param service for converting to other reactive types from Flux and Mono * @param validator validator to validate decoded objects with */ protected AbstractMessageConverterArgumentResolver(List<HttpMessageConverter<?>> converters, ConversionService service, Validator validator) { Assert.notEmpty(converters, "At least one message converter is required."); Assert.notNull(service, "'conversionService' is required."); this.messageConverters = converters; this.conversionService = service; this.validator = validator; this.supportedMediaTypes = converters.stream() .flatMap(converter -> converter.getReadableMediaTypes().stream()) .collect(Collectors.toList()); } /** * Return the configured message converters. */ public List<HttpMessageConverter<?>> getMessageConverters() { return this.messageConverters; } /** * Return the configured {@link ConversionService}. */ public ConversionService getConversionService() { return this.conversionService; } protected Mono<Object> readBody(MethodParameter bodyParameter, boolean isBodyRequired, ServerWebExchange exchange) { TypeDescriptor typeDescriptor = new TypeDescriptor(bodyParameter); boolean convertFromMono = getConversionService().canConvert(MONO_TYPE, typeDescriptor); boolean convertFromFlux = getConversionService().canConvert(FLUX_TYPE, typeDescriptor); ResolvableType elementType = ResolvableType.forMethodParameter(bodyParameter); if (convertFromMono || convertFromFlux) { elementType = elementType.getGeneric(0); } ServerHttpRequest request = exchange.getRequest(); MediaType mediaType = request.getHeaders().getContentType(); if (mediaType == null) { mediaType = MediaType.APPLICATION_OCTET_STREAM; } for (HttpMessageConverter<?> converter : getMessageConverters()) { if (converter.canRead(elementType, mediaType)) { if (convertFromFlux) { Flux<?> flux = converter.read(elementType, request) .onErrorResumeWith(ex -> Flux.error(getReadError(ex, bodyParameter))); if (checkRequired(bodyParameter, isBodyRequired)) { flux = flux.switchIfEmpty(Flux.error(getRequiredBodyError(bodyParameter))); } if (this.validator != null) { flux = flux.map(applyValidationIfApplicable(bodyParameter)); } return Mono.just(getConversionService().convert(flux, FLUX_TYPE, typeDescriptor)); } else { Mono<?> mono = converter.readMono(elementType, request) .otherwise(ex -> Mono.error(getReadError(ex, bodyParameter))); if (checkRequired(bodyParameter, isBodyRequired)) { mono = mono.otherwiseIfEmpty(Mono.error(getRequiredBodyError(bodyParameter))); } if (this.validator != null) { mono = mono.map(applyValidationIfApplicable(bodyParameter)); } if (convertFromMono) { return Mono.just(getConversionService().convert(mono, MONO_TYPE, typeDescriptor)); } else { return Mono.from(mono); } } } } return Mono.error(new UnsupportedMediaTypeStatusException(mediaType, this.supportedMediaTypes)); } protected boolean checkRequired(MethodParameter bodyParameter, boolean isBodyRequired) { if ("rx.Single".equals(bodyParameter.getNestedParameterType().getName())) { return true; } return isBodyRequired; } protected ServerWebInputException getReadError(Throwable ex, MethodParameter parameter) { return new ServerWebInputException("Failed to read HTTP message", parameter, ex); } protected ServerWebInputException getRequiredBodyError(MethodParameter parameter) { return new ServerWebInputException("Required request body is missing: " + parameter.getMethod().toGenericString()); } protected <T> Function<T, T> applyValidationIfApplicable(MethodParameter methodParam) { Annotation[] annotations = methodParam.getParameterAnnotations(); for (Annotation ann : annotations) { Validated validAnnot = AnnotationUtils.getAnnotation(ann, Validated.class); if (validAnnot != null || ann.annotationType().getSimpleName().startsWith("Valid")) { Object hints = (validAnnot != null ? validAnnot.value() : AnnotationUtils.getValue(ann)); Object[] validHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); return element -> { doValidate(element, validHints, methodParam); return element; }; } } return element -> element; } /** * TODO: replace with use of DataBinder */ private void doValidate(Object target, Object[] validationHints, MethodParameter methodParam) { String name = Conventions.getVariableNameForParameter(methodParam); Errors errors = new BeanPropertyBindingResult(target, name); if (!ObjectUtils.isEmpty(validationHints) && this.validator instanceof SmartValidator) { ((SmartValidator) this.validator).validate(target, errors, validationHints); } else if (this.validator != null) { this.validator.validate(target, errors); } if (errors.hasErrors()) { throw new ServerWebInputException("Validation failed", methodParam); } } }