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}