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.util.Collections;
020import java.util.List;
021
022import reactor.core.publisher.Flux;
023import reactor.core.publisher.Mono;
024
025import org.springframework.core.MethodParameter;
026import org.springframework.core.ReactiveAdapter;
027import org.springframework.core.ReactiveAdapterRegistry;
028import org.springframework.core.io.buffer.DataBuffer;
029import org.springframework.http.HttpHeaders;
030import org.springframework.http.codec.HttpMessageReader;
031import org.springframework.http.codec.multipart.Part;
032import org.springframework.http.server.reactive.ServerHttpRequest;
033import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
034import org.springframework.lang.Nullable;
035import org.springframework.util.CollectionUtils;
036import org.springframework.web.bind.annotation.RequestPart;
037import org.springframework.web.reactive.BindingContext;
038import org.springframework.web.server.ServerWebExchange;
039import org.springframework.web.server.ServerWebInputException;
040
041/**
042 * Resolver for {@code @RequestPart} arguments where the named part is decoded
043 * much like an {@code @RequestBody} argument but based on the content of an
044 * individual part instead. The arguments may be wrapped with a reactive type
045 * for a single value (e.g. Reactor {@code Mono}, RxJava {@code Single}).
046 *
047 * <p>This resolver also supports arguments of type {@link Part} which may be
048 * wrapped with are reactive type for a single or multiple values.
049 *
050 * @author Rossen Stoyanchev
051 * @since 5.0
052 */
053public class RequestPartMethodArgumentResolver extends AbstractMessageReaderArgumentResolver {
054
055        public RequestPartMethodArgumentResolver(List<HttpMessageReader<?>> readers, ReactiveAdapterRegistry registry) {
056                super(readers, registry);
057        }
058
059
060        @Override
061        public boolean supportsParameter(MethodParameter parameter) {
062                return (parameter.hasParameterAnnotation(RequestPart.class) ||
063                                checkParameterType(parameter, Part.class::isAssignableFrom));
064        }
065
066        @Override
067        public Mono<Object> resolveArgument(
068                        MethodParameter parameter, BindingContext bindingContext, ServerWebExchange exchange) {
069
070                RequestPart requestPart = parameter.getParameterAnnotation(RequestPart.class);
071                boolean isRequired = (requestPart == null || requestPart.required());
072                String name = getPartName(parameter, requestPart);
073
074                Flux<Part> parts = exchange.getMultipartData()
075                                .flatMapIterable(map -> {
076                                        List<Part> list = map.get(name);
077                                        if (CollectionUtils.isEmpty(list)) {
078                                                if (isRequired) {
079                                                        throw getMissingPartException(name, parameter);
080                                                }
081                                                return Collections.emptyList();
082                                        }
083                                        return list;
084                                });
085
086                if (Part.class.isAssignableFrom(parameter.getParameterType())) {
087                        return parts.next().cast(Object.class);
088                }
089
090                if (List.class.isAssignableFrom(parameter.getParameterType())) {
091                        MethodParameter elementType = parameter.nested();
092                        if (Part.class.isAssignableFrom(elementType.getNestedParameterType())) {
093                                return parts.collectList().cast(Object.class);
094                        }
095                        else {
096                                return decodePartValues(parts, elementType, bindingContext, exchange, isRequired)
097                                                .collectList().cast(Object.class);
098                        }
099                }
100
101                ReactiveAdapter adapter = getAdapterRegistry().getAdapter(parameter.getParameterType());
102                if (adapter != null) {
103                        MethodParameter elementType = parameter.nested();
104                        return Mono.just(adapter.fromPublisher(
105                                        Part.class.isAssignableFrom(elementType.getNestedParameterType()) ?
106                                                        parts : decodePartValues(parts, elementType, bindingContext, exchange, isRequired)));
107                }
108
109                return decodePartValues(parts, parameter, bindingContext, exchange, isRequired)
110                                .next().cast(Object.class);
111        }
112
113        private String getPartName(MethodParameter methodParam, @Nullable RequestPart requestPart) {
114                String partName = (requestPart != null ? requestPart.name() : "");
115                if (partName.isEmpty()) {
116                        partName = methodParam.getParameterName();
117                        if (partName == null) {
118                                throw new IllegalArgumentException("Request part name for argument type [" +
119                                                methodParam.getNestedParameterType().getName() +
120                                                "] not specified, and parameter name information not found in class file either.");
121                        }
122                }
123                return partName;
124        }
125
126        private ServerWebInputException getMissingPartException(String name, MethodParameter param) {
127                String reason = "Required request part '" + name + "' is not present";
128                return new ServerWebInputException(reason, param);
129        }
130
131
132        private Flux<?> decodePartValues(Flux<Part> parts, MethodParameter elementType, BindingContext bindingContext,
133                        ServerWebExchange exchange, boolean isRequired) {
134
135                return parts.flatMap(part -> {
136                        ServerHttpRequest partRequest = new PartServerHttpRequest(exchange.getRequest(), part);
137                        ServerWebExchange partExchange = exchange.mutate().request(partRequest).build();
138                        if (logger.isDebugEnabled()) {
139                                logger.debug(exchange.getLogPrefix() + "Decoding part '" + part.name() + "'");
140                        }
141                        return readBody(elementType, isRequired, bindingContext, partExchange);
142                });
143        }
144
145
146        private static class PartServerHttpRequest extends ServerHttpRequestDecorator {
147
148                private final Part part;
149
150                public PartServerHttpRequest(ServerHttpRequest delegate, Part part) {
151                        super(delegate);
152                        this.part = part;
153                }
154
155                @Override
156                public HttpHeaders getHeaders() {
157                        return this.part.headers();
158                }
159
160                @Override
161                public Flux<DataBuffer> getBody() {
162                        return this.part.content();
163                }
164        }
165
166}