Skip to content

Commit

Permalink
Add MultipartFile support to HTTP interface client
Browse files Browse the repository at this point in the history
  • Loading branch information
OlgaMaciaszek authored and rstoyanchev committed Jun 27, 2023
1 parent 3f40452 commit e69a1d2
Show file tree
Hide file tree
Showing 4 changed files with 200 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ private List<HttpServiceArgumentResolver> initArgumentResolvers() {
// Specific type
resolvers.add(new UrlArgumentResolver());
resolvers.add(new HttpMethodArgumentResolver());
resolvers.add(new MultipartFileArgumentResolver());

return resolvers;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright 2002-2023 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
*
* https://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.service.invoker;

import java.util.Optional;

import org.springframework.core.MethodParameter;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.util.Assert;
import org.springframework.web.multipart.MultipartFile;

/**
* {@link HttpServiceArgumentResolver} for arguments of type {@link MultipartFile}.
* The arguments should not be annotated. To allow for non-required arguments,
* the {@link MultipartFile} parameters can also be wrapped with {@link Optional}.
*
* @author Olga Maciaszek-Sharma
* @since 6.1
*/
public class MultipartFileArgumentResolver extends AbstractNamedValueArgumentResolver {

private static final String MULTIPART_FILE_LABEL = "multipart file";

@Override
protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) {
if (!parameter.nestedIfOptional().getNestedParameterType().equals(MultipartFile.class)) {
return null;
}
return new NamedValueInfo("", true, null, MULTIPART_FILE_LABEL, true);

}

@Override
protected void addRequestValue(String name, Object value, MethodParameter parameter,
HttpRequestValues.Builder requestValues) {
Assert.state(value instanceof MultipartFile,
"The value has to be of type 'MultipartFile'");

MultipartFile file = (MultipartFile) value;
requestValues.addRequestPart(name, toHttpEntity(name, file));
}

private HttpEntity<Resource> toHttpEntity(String name, MultipartFile file) {
HttpHeaders headers = new HttpHeaders();
if (file.getOriginalFilename() != null) {
headers.setContentDispositionFormData(name, file.getOriginalFilename());
}
if (file.getContentType() != null) {
headers.add(HttpHeaders.CONTENT_TYPE, file.getContentType());
}
return new HttpEntity<>(file.getResource(), headers);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright 2002-2023 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
*
* https://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.service.invoker;

import java.util.Optional;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import org.springframework.http.ContentDisposition;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.service.annotation.PostExchange;
import org.springframework.web.testfixture.servlet.MockMultipartFile;

import static org.assertj.core.api.Assertions.assertThat;

/**
* Unit tests for {@link MultipartFileArgumentResolver}.
* Tests for base class functionality of this resolver can be found in {@link NamedValueArgumentResolverTests}.
*
* @author Olga Maciaszek-Sharma
*/
@SuppressWarnings("unchecked")
class MultipartFileArgumentResolverTests {

private final TestHttpClientAdapter clientAdapter = new TestHttpClientAdapter();

private TestClient client;

@BeforeEach
void setUp() {
HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory.builder(this.clientAdapter).build();
this.client = proxyFactory.createClient(TestClient.class);
}

@Test
void multipartFile() {
String fileName = "testFileName";
String originalFileName = "originalTestFileName";
MultipartFile testFile = new MockMultipartFile(fileName, originalFileName,
MediaType.APPLICATION_JSON_VALUE, "test".getBytes());
this.client.postMultipartFile(testFile);

Object body = clientAdapter.getRequestValues().getBodyValue();

assertThat(body).isInstanceOf(MultiValueMap.class);
MultiValueMap<String, HttpEntity<?>> map = (MultiValueMap<String, HttpEntity<?>>) body;
assertThat(map.size()).isEqualTo(1);
assertThat(map.getFirst("file")).isNotNull();
HttpEntity<?> fileEntity = map.getFirst("file");
assertThat(fileEntity.getBody()).isEqualTo(testFile.getResource());
HttpHeaders headers = fileEntity.getHeaders();
assertThat(headers.getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
ContentDisposition contentDisposition = headers.getContentDisposition();
assertThat(contentDisposition.getType()).isEqualTo("form-data");
assertThat(contentDisposition.getName()).isEqualTo("file");
assertThat(contentDisposition.getFilename()).isEqualTo(originalFileName);
}

@Test
void optionalMultipartFile() {
this.client.postOptionalMultipartFile(Optional.empty(), "anotherPart");

Object body = clientAdapter.getRequestValues().getBodyValue();

assertThat(body).isInstanceOf(MultiValueMap.class);
MultiValueMap<String, HttpEntity<?>> map = (MultiValueMap<String, HttpEntity<?>>) body;
assertThat(map.size()).isEqualTo(1);
assertThat(map.getFirst("anotherPart")).isNotNull();
}

@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
private interface TestClient {

@PostExchange
void postMultipartFile(MultipartFile file);

@PostExchange
void postOptionalMultipartFile(Optional<MultipartFile> file, @RequestPart String anotherPart);

}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
Expand Down Expand Up @@ -33,16 +33,20 @@
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;

import org.springframework.http.MediaType;
import org.springframework.lang.Nullable;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestAttribute;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.PostExchange;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
import org.springframework.web.testfixture.servlet.MockMultipartFile;

import static org.assertj.core.api.Assertions.assertThat;

Expand All @@ -52,6 +56,7 @@
* using {@link WebClient} and {@link MockWebServer}.
*
* @author Rossen Stoyanchev
* @author Olga Maciaszek-Sharma
*/
public class WebClientHttpServiceProxyTests {

Expand Down Expand Up @@ -133,6 +138,25 @@ void formData() throws Exception {
assertThat(request.getBody().readUtf8()).isEqualTo("param1=value+1&param2=value+2");
}

@Test // gh-30342
void multipart() throws InterruptedException {
prepareResponse(response -> response.setResponseCode(201));
String fileName = "testFileName";
String originalFileName = "originalTestFileName";
MultipartFile file = new MockMultipartFile(fileName, originalFileName,
MediaType.APPLICATION_JSON_VALUE, "test".getBytes());

initHttpService().postMultipart(file, "test2");

RecordedRequest request = this.server.takeRequest();
assertThat(request.getHeaders().get("Content-Type")).startsWith("multipart/form-data;boundary=");
assertThat(request.getBody().readUtf8())
.containsSubsequence("Content-Disposition: form-data; name=\"file\"; filename=\"originalTestFileName\"",
"Content-Type: application/json", "Content-Length: 4", "test",
"Content-Disposition: form-data; name=\"anotherPart\"",
"Content-Type: text/plain;charset=UTF-8", "Content-Length: 5", "test2");
}

private TestHttpService initHttpService() {
WebClient webClient = WebClient.builder().baseUrl(this.server.url("/").toString()).build();
return initHttpService(webClient);
Expand Down Expand Up @@ -166,6 +190,9 @@ private interface TestHttpService {
@PostExchange(contentType = "application/x-www-form-urlencoded")
void postForm(@RequestParam MultiValueMap<String, String> params);

@PostExchange(contentType = MediaType.MULTIPART_FORM_DATA_VALUE)
void postMultipart(MultipartFile file, @RequestPart String anotherPart);

}

}

0 comments on commit e69a1d2

Please sign in to comment.