7 changed files with 316 additions and 4 deletions
@ -0,0 +1,128 @@
@@ -0,0 +1,128 @@
|
||||
/* |
||||
* 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.util.List; |
||||
import java.util.Map; |
||||
import java.util.Optional; |
||||
|
||||
import org.springframework.beans.BeanUtils; |
||||
import org.springframework.beans.factory.config.ConfigurableBeanFactory; |
||||
import org.springframework.core.MethodParameter; |
||||
import org.springframework.core.ReactiveAdapterRegistry; |
||||
import org.springframework.core.convert.converter.Converter; |
||||
import org.springframework.http.codec.multipart.Part; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.util.MultiValueMap; |
||||
import org.springframework.util.StringUtils; |
||||
import org.springframework.web.bind.annotation.RequestParam; |
||||
import org.springframework.web.bind.annotation.RequestPart; |
||||
import org.springframework.web.bind.annotation.ValueConstants; |
||||
import org.springframework.web.server.ServerWebExchange; |
||||
import org.springframework.web.server.ServerWebInputException; |
||||
|
||||
/** |
||||
* Resolver for method arguments annotated with @{@link RequestPart}. |
||||
* |
||||
* @author Sebastien Deleuze |
||||
* @since 5.0 |
||||
* @see RequestParamMapMethodArgumentResolver |
||||
*/ |
||||
public class RequestPartMethodArgumentResolver extends AbstractNamedValueSyncArgumentResolver { |
||||
|
||||
private final boolean useDefaultResolution; |
||||
|
||||
|
||||
/** |
||||
* Class constructor with a default resolution mode flag. |
||||
* @param factory a bean factory used for resolving ${...} placeholder |
||||
* and #{...} SpEL expressions in default values, or {@code null} if default |
||||
* values are not expected to contain expressions |
||||
* @param registry for checking reactive type wrappers |
||||
* @param useDefaultResolution in default resolution mode a method argument |
||||
* that is a simple type, as defined in {@link BeanUtils#isSimpleProperty}, |
||||
* is treated as a request parameter even if it isn't annotated, the |
||||
* request parameter name is derived from the method parameter name. |
||||
*/ |
||||
public RequestPartMethodArgumentResolver( |
||||
ConfigurableBeanFactory factory, ReactiveAdapterRegistry registry, boolean useDefaultResolution) { |
||||
|
||||
super(factory, registry); |
||||
this.useDefaultResolution = useDefaultResolution; |
||||
} |
||||
|
||||
|
||||
@Override |
||||
public boolean supportsParameter(MethodParameter param) { |
||||
if (checkAnnotatedParamNoReactiveWrapper(param, RequestPart.class, this::singleParam)) { |
||||
return true; |
||||
} |
||||
else if (this.useDefaultResolution) { |
||||
return checkParameterTypeNoReactiveWrapper(param, BeanUtils::isSimpleProperty) || |
||||
BeanUtils.isSimpleProperty(param.nestedIfOptional().getNestedParameterType()); |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
private boolean singleParam(RequestPart requestParam, Class<?> type) { |
||||
return !Map.class.isAssignableFrom(type) || StringUtils.hasText(requestParam.name()); |
||||
} |
||||
|
||||
@Override |
||||
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { |
||||
RequestPart ann = parameter.getParameterAnnotation(RequestPart.class); |
||||
return (ann != null ? new RequestPartNamedValueInfo(ann) : new RequestPartNamedValueInfo()); |
||||
} |
||||
|
||||
@Override |
||||
protected Optional<Object> resolveNamedValue(String name, MethodParameter parameter, |
||||
ServerWebExchange exchange) { |
||||
|
||||
List<?> paramValues = getMultipartData(exchange).get(name); |
||||
Object result = null; |
||||
if (paramValues != null) { |
||||
result = (paramValues.size() == 1 ? paramValues.get(0) : paramValues); |
||||
} |
||||
return Optional.ofNullable(result); |
||||
} |
||||
|
||||
private MultiValueMap<String, Part> getMultipartData(ServerWebExchange exchange) { |
||||
MultiValueMap<String, Part> params = exchange.getMultipartData().subscribe().peek(); |
||||
Assert.notNull(params, "Expected multipart data (if any) to be parsed."); |
||||
return params; |
||||
} |
||||
|
||||
@Override |
||||
protected void handleMissingValue(String name, MethodParameter parameter, ServerWebExchange exchange) { |
||||
String type = parameter.getNestedParameterType().getSimpleName(); |
||||
String reason = "Required " + type + " parameter '" + name + "' is not present"; |
||||
throw new ServerWebInputException(reason, parameter); |
||||
} |
||||
|
||||
|
||||
private static class RequestPartNamedValueInfo extends NamedValueInfo { |
||||
|
||||
RequestPartNamedValueInfo() { |
||||
super("", false, ValueConstants.DEFAULT_NONE); |
||||
} |
||||
|
||||
RequestPartNamedValueInfo(RequestPart annotation) { |
||||
super(annotation.name(), annotation.required(), ValueConstants.DEFAULT_NONE); |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,163 @@
@@ -0,0 +1,163 @@
|
||||
/* |
||||
* 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.util.Map; |
||||
|
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
import reactor.core.publisher.Mono; |
||||
import reactor.test.StepVerifier; |
||||
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.core.io.ClassPathResource; |
||||
import org.springframework.http.HttpEntity; |
||||
import org.springframework.http.HttpHeaders; |
||||
import org.springframework.http.HttpStatus; |
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.http.codec.multipart.Part; |
||||
import org.springframework.http.server.reactive.AbstractHttpHandlerIntegrationTests; |
||||
import org.springframework.http.server.reactive.HttpHandler; |
||||
import org.springframework.util.LinkedMultiValueMap; |
||||
import org.springframework.util.MultiValueMap; |
||||
import org.springframework.web.bind.annotation.PostMapping; |
||||
import org.springframework.web.bind.annotation.RequestParam; |
||||
import org.springframework.web.bind.annotation.RequestPart; |
||||
import org.springframework.web.bind.annotation.RestController; |
||||
import org.springframework.web.reactive.DispatcherHandler; |
||||
import org.springframework.web.reactive.config.EnableWebFlux; |
||||
import org.springframework.web.reactive.function.BodyInserters; |
||||
import org.springframework.web.reactive.function.client.ClientResponse; |
||||
import org.springframework.web.reactive.function.client.WebClient; |
||||
import org.springframework.web.server.adapter.WebHttpHandlerBuilder; |
||||
|
||||
import static org.junit.Assert.assertEquals; |
||||
|
||||
public class MultipartIntegrationTests extends AbstractHttpHandlerIntegrationTests { |
||||
|
||||
private AnnotationConfigApplicationContext wac; |
||||
|
||||
private WebClient webClient; |
||||
|
||||
@Override |
||||
@Before |
||||
public void setup() throws Exception { |
||||
super.setup(); |
||||
this.webClient = WebClient.create("http://localhost:" + this.port); |
||||
} |
||||
|
||||
|
||||
@Override |
||||
protected HttpHandler createHttpHandler() { |
||||
this.wac = new AnnotationConfigApplicationContext(); |
||||
this.wac.register(TestConfiguration.class); |
||||
this.wac.refresh(); |
||||
|
||||
return WebHttpHandlerBuilder.webHandler(new DispatcherHandler(this.wac)).build(); |
||||
} |
||||
|
||||
@Test |
||||
public void map() { |
||||
test("/map"); |
||||
} |
||||
|
||||
@Test |
||||
public void multiValueMap() { |
||||
test("/multivaluemap"); |
||||
} |
||||
|
||||
@Test |
||||
public void partParam() { |
||||
test("/partparam"); |
||||
} |
||||
|
||||
@Test |
||||
public void part() { |
||||
test("/part"); |
||||
} |
||||
|
||||
private void test(String uri) { |
||||
Mono<ClientResponse> result = webClient |
||||
.post() |
||||
.uri(uri) |
||||
.contentType(MediaType.MULTIPART_FORM_DATA) |
||||
.body(BodyInserters.fromMultipartData(generateBody())) |
||||
.exchange(); |
||||
|
||||
StepVerifier |
||||
.create(result) |
||||
.consumeNextWith(response -> assertEquals(HttpStatus.OK, response.statusCode())) |
||||
.verifyComplete(); |
||||
} |
||||
|
||||
private MultiValueMap<String, Object> generateBody() { |
||||
HttpHeaders fooHeaders = new HttpHeaders(); |
||||
fooHeaders.setContentType(MediaType.TEXT_PLAIN); |
||||
ClassPathResource fooResource = new ClassPathResource("org/springframework/http/codec/multipart/foo.txt"); |
||||
HttpEntity<ClassPathResource> fooPart = new HttpEntity<>(fooResource, fooHeaders); |
||||
HttpEntity<String> barPart = new HttpEntity<>("bar"); |
||||
MultiValueMap<String, Object> parts = new LinkedMultiValueMap<>(); |
||||
parts.add("fooPart", fooPart); |
||||
parts.add("barPart", barPart); |
||||
return parts; |
||||
} |
||||
|
||||
@RestController |
||||
@SuppressWarnings("unused") |
||||
static class MultipartController { |
||||
|
||||
@PostMapping("/map") |
||||
void map(@RequestParam Map<String, Part> parts) { |
||||
assertEquals(2, parts.size()); |
||||
assertEquals("foo.txt", parts.get("fooPart").getFilename().get()); |
||||
assertEquals("bar", parts.get("barPart").getContentAsString().block()); |
||||
} |
||||
|
||||
@PostMapping("/multivaluemap") |
||||
void multiValueMap(@RequestParam MultiValueMap<String, Part> parts) { |
||||
Map<String, Part> map = parts.toSingleValueMap(); |
||||
assertEquals(2, map.size()); |
||||
assertEquals("foo.txt", map.get("fooPart").getFilename().get()); |
||||
assertEquals("bar", map.get("barPart").getContentAsString().block()); |
||||
} |
||||
|
||||
@PostMapping("/partparam") |
||||
void partParam(@RequestParam Part fooPart) { |
||||
assertEquals("foo.txt", fooPart.getFilename().get()); |
||||
} |
||||
|
||||
@PostMapping("/part") |
||||
void part(@RequestPart Part fooPart) { |
||||
assertEquals("foo.txt", fooPart.getFilename().get()); |
||||
} |
||||
|
||||
} |
||||
|
||||
@Configuration |
||||
@EnableWebFlux |
||||
@SuppressWarnings("unused") |
||||
static class TestConfiguration { |
||||
|
||||
@Bean |
||||
public MultipartController multipartController() { |
||||
return new MultipartController(); |
||||
} |
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue