/*
* Copyright 2002-2017 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.Collections;
import java.util.List;
import java.util.Map;
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.ReactiveAdapter;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.codec.DecodingException;
import org.springframework.http.MediaType;
import org.springframework.http.codec.HttpMessageReader;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.util.Assert;
import org.springframework.validation.Validator;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.support.WebExchangeBindException;
import org.springframework.web.bind.support.WebExchangeDataBinder;
import org.springframework.web.reactive.BindingContext;
import org.springframework.web.reactive.result.method.HandlerMethodArgumentResolverSupport;
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 HttpMessageReader}.
*
* <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
* @since 5.0
*/
public abstract class AbstractMessageReaderArgumentResolver extends HandlerMethodArgumentResolverSupport {
private final List<HttpMessageReader<?>> messageReaders;
private final List<MediaType> supportedMediaTypes;
/**
* Constructor with {@link HttpMessageReader}'s and a {@link Validator}.
* @param readers readers to convert from the request body
*/
protected AbstractMessageReaderArgumentResolver(List<HttpMessageReader<?>> readers) {
this(readers, new ReactiveAdapterRegistry());
}
/**
* Constructor that also accepts a {@link ReactiveAdapterRegistry}.
* @param messageReaders readers to convert from the request body
* @param adapterRegistry for adapting to other reactive types from Flux and Mono
*/
protected AbstractMessageReaderArgumentResolver(List<HttpMessageReader<?>> messageReaders,
ReactiveAdapterRegistry adapterRegistry) {
super(adapterRegistry);
Assert.notEmpty(messageReaders, "At least one HttpMessageReader is required");
Assert.notNull(adapterRegistry, "ReactiveAdapterRegistry is required");
this.messageReaders = messageReaders;
this.supportedMediaTypes = messageReaders.stream()
.flatMap(converter -> converter.getReadableMediaTypes().stream())
.collect(Collectors.toList());
}
/**
* Return the configured message converters.
*/
public List<HttpMessageReader<?>> getMessageReaders() {
return this.messageReaders;
}
protected Mono<Object> readBody(MethodParameter bodyParameter, boolean isBodyRequired,
BindingContext bindingContext, ServerWebExchange exchange) {
ResolvableType bodyType = ResolvableType.forMethodParameter(bodyParameter);
ReactiveAdapter adapter = getAdapterRegistry().getAdapter(bodyType.resolve());
ResolvableType elementType = (adapter != null ? bodyType.getGeneric(0) : bodyType);
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
MediaType mediaType = request.getHeaders().getContentType();
if (mediaType == null) {
mediaType = MediaType.APPLICATION_OCTET_STREAM;
}
for (HttpMessageReader<?> reader : getMessageReaders()) {
if (reader.canRead(elementType, mediaType)) {
Map<String, Object> readHints = Collections.emptyMap();
if (adapter != null && adapter.isMultiValue()) {
Flux<?> flux = reader.read(bodyType, elementType, request, response, readHints);
flux = flux.onErrorResume(ex -> Flux.error(handleReadError(bodyParameter, ex)));
if (isBodyRequired || !adapter.supportsEmpty()) {
flux = flux.switchIfEmpty(Flux.error(handleMissingBody(bodyParameter)));
}
Object[] hints = extractValidationHints(bodyParameter);
if (hints != null) {
flux = flux.doOnNext(target ->
validate(target, hints, bodyParameter, bindingContext, exchange));
}
return Mono.just(adapter.fromPublisher(flux));
}
else {
Mono<?> mono = reader.readMono(bodyType, elementType, request, response, readHints);
mono = mono.onErrorResume(ex -> Mono.error(handleReadError(bodyParameter, ex)));
if (isBodyRequired || (adapter != null && !adapter.supportsEmpty())) {
mono = mono.switchIfEmpty(Mono.error(handleMissingBody(bodyParameter)));
}
Object[] hints = extractValidationHints(bodyParameter);
if (hints != null) {
mono = mono.doOnNext(target ->
validate(target, hints, bodyParameter, bindingContext, exchange));
}
if (adapter != null) {
return Mono.just(adapter.fromPublisher(mono));
}
else {
return Mono.from(mono);
}
}
}
}
return Mono.error(new UnsupportedMediaTypeStatusException(mediaType, this.supportedMediaTypes));
}
private Throwable handleReadError(MethodParameter parameter, Throwable ex) {
return (ex instanceof DecodingException ?
new ServerWebInputException("Failed to read HTTP message", parameter, ex) : ex);
}
private ServerWebInputException handleMissingBody(MethodParameter param) {
return new ServerWebInputException("Request body is missing: " + param.getMethod().toGenericString());
}
/**
* Check if the given MethodParameter requires validation and if so return
* a (possibly empty) Object[] with validation hints. A return value of
* {@code null} indicates that validation is not required.
*/
private Object[] extractValidationHints(MethodParameter parameter) {
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation ann : annotations) {
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
return (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
}
}
return null;
}
private void validate(Object target, Object[] validationHints, MethodParameter param,
BindingContext binding, ServerWebExchange exchange) {
String name = Conventions.getVariableNameForParameter(param);
WebExchangeDataBinder binder = binding.createDataBinder(exchange, target, name);
binder.validate(validationHints);
if (binder.getBindingResult().hasErrors()) {
throw new WebExchangeBindException(param, binder.getBindingResult());
}
}
}