diff --git a/feign-form-spring/pom.xml b/feign-form-spring/pom.xml index 17a9ff9..ffc75ee 100644 --- a/feign-form-spring/pom.xml +++ b/feign-form-spring/pom.xml @@ -44,7 +44,7 @@ limitations under the License. org.springframework spring-web - 5.1.5.RELEASE + 5.2.5.RELEASE compile @@ -58,13 +58,13 @@ limitations under the License. org.springframework.boot spring-boot-starter-web - 2.1.3.RELEASE + 2.2.6.RELEASE test org.springframework.cloud spring-cloud-starter-openfeign - 2.1.1.RELEASE + 2.2.2.RELEASE test diff --git a/feign-form-spring/src/main/java/feign/form/spring/PojoSerializationWriter.java b/feign-form-spring/src/main/java/feign/form/spring/PojoSerializationWriter.java new file mode 100644 index 0000000..fca4472 --- /dev/null +++ b/feign-form-spring/src/main/java/feign/form/spring/PojoSerializationWriter.java @@ -0,0 +1,89 @@ +/* + * Copyright 2020 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 feign.form.spring; + +import static feign.form.ContentProcessor.CRLF; +import static feign.form.util.PojoUtil.isUserPojo; + +import org.springframework.http.MediaType; +import org.springframework.web.multipart.MultipartFile; + +import feign.codec.EncodeException; +import feign.form.multipart.AbstractWriter; +import feign.form.multipart.Output; + +import lombok.val; + +import java.io.IOException; + +/** + * + * @author Darren Foong + */ +public abstract class PojoSerializationWriter extends AbstractWriter { + @Override + public boolean isApplicable(Object object) { + return !(object instanceof MultipartFile) && !(object instanceof MultipartFile[]) + && (isUserPojoCollection(object) || isUserPojo(object)); + } + + @Override + public void write (Output output, String key, Object object) throws EncodeException { + try { + val string = new StringBuilder() + .append("Content-Disposition: form-data; name=\"").append(key).append('"') + .append(CRLF) + .append("Content-Type: ").append(getContentType()) + .append("; charset=").append(output.getCharset().name()) + .append(CRLF) + .append(CRLF) + .append(serialize(object)) + .toString(); + + output.write(string); + } catch (IOException e) { + throw new EncodeException(e.getMessage()); + } + } + + protected abstract MediaType getContentType(); + + protected abstract String serialize(Object object) throws IOException; + + private boolean isUserPojoCollection(Object object) { + if (object.getClass().isArray()) { + val array = (Object[]) object; + + return array.length > 1 && isUserPojo(array[0]); + } + + if (!(object instanceof Iterable)) { + return false; + } + + val iterable = (Iterable) object; + val iterator = iterable.iterator(); + + if (iterator.hasNext()) { + val next = iterator.next(); + + return !(next instanceof MultipartFile) && isUserPojo(next); + } else { + return false; + } + } +} diff --git a/feign-form-spring/src/main/java/feign/form/spring/SpringFormEncoder.java b/feign-form-spring/src/main/java/feign/form/spring/SpringFormEncoder.java index 2ce3321..fe0cbce 100644 --- a/feign-form-spring/src/main/java/feign/form/spring/SpringFormEncoder.java +++ b/feign-form-spring/src/main/java/feign/form/spring/SpringFormEncoder.java @@ -59,38 +59,23 @@ public SpringFormEncoder (Encoder delegate) { processor.addFirstWriter(new SpringManyMultipartFilesWriter()); } + public SpringFormEncoder(PojoSerializationWriter pojoSerializationWriter, Encoder delegate) { + super(delegate); + + val processor = (MultipartFormContentProcessor) getContentProcessor(MULTIPART); + processor.addFirstWriter(new SpringSingleMultipartFileWriter()); + processor.addFirstWriter(new SpringManyMultipartFilesWriter()); + processor.addFirstWriter(pojoSerializationWriter); + } + @Override public void encode (Object object, Type bodyType, RequestTemplate template) throws EncodeException { - if (bodyType.equals(MultipartFile[].class)) { - val files = (MultipartFile[]) object; - val data = new HashMap(files.length, 1.F); - for (val file : files) { - data.put(file.getName(), file); - } - super.encode(data, MAP_STRING_WILDCARD, template); - } else if (bodyType.equals(MultipartFile.class)) { + if (bodyType.equals(MultipartFile.class)) { val file = (MultipartFile) object; val data = singletonMap(file.getName(), object); super.encode(data, MAP_STRING_WILDCARD, template); - } else if (isMultipartFileCollection(object)) { - val iterable = (Iterable) object; - val data = new HashMap(); - for (val item : iterable) { - val file = (MultipartFile) item; - data.put(file.getName(), file); - } - super.encode(data, MAP_STRING_WILDCARD, template); } else { super.encode(object, bodyType, template); } } - - private boolean isMultipartFileCollection (Object object) { - if (!(object instanceof Iterable)) { - return false; - } - val iterable = (Iterable) object; - val iterator = iterable.iterator(); - return iterator.hasNext() && iterator.next() instanceof MultipartFile; - } } diff --git a/feign-form-spring/src/test/java/feign/form/feign/spring/Client.java b/feign-form-spring/src/test/java/feign/form/feign/spring/Client.java index d116fac..69d03c6 100644 --- a/feign-form-spring/src/test/java/feign/form/feign/spring/Client.java +++ b/feign-form-spring/src/test/java/feign/form/feign/spring/Client.java @@ -20,12 +20,15 @@ import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE; import static org.springframework.web.bind.annotation.RequestMethod.POST; +import java.io.IOException; import java.util.List; import java.util.Map; +import com.fasterxml.jackson.databind.ObjectMapper; import feign.Logger; import feign.Response; import feign.codec.Encoder; +import feign.form.spring.PojoSerializationWriter; import feign.form.spring.SpringFormEncoder; import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -33,6 +36,7 @@ import org.springframework.cloud.openfeign.FeignClient; import org.springframework.cloud.openfeign.support.SpringEncoder; import org.springframework.context.annotation.Bean; +import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -57,7 +61,7 @@ public interface Client { consumes = MULTIPART_FORM_DATA_VALUE ) String upload1 (@PathVariable("folder") String folder, - @RequestPart MultipartFile file, + @RequestPart("file") MultipartFile file, @RequestParam(value = "message", required = false) String message); @RequestMapping( @@ -99,14 +103,42 @@ String upload4 (@PathVariable("id") String id, method = POST, consumes = MULTIPART_FORM_DATA_VALUE ) - String upload6Array (@RequestPart MultipartFile[] files); + String upload6Array (@RequestPart("files") MultipartFile[] files); @RequestMapping( path = "/multipart/upload6", method = POST, consumes = MULTIPART_FORM_DATA_VALUE ) - String upload6Collection (@RequestPart List files); + String upload6Collection (@RequestPart("files") List files); + + @RequestMapping( + path = "/multipart/upload7", + method = POST, + consumes = MULTIPART_FORM_DATA_VALUE + ) + String upload7 (@RequestPart("pojo") Pojo pojo); + + @RequestMapping( + path = "/multipart/upload8", + method = POST, + consumes = MULTIPART_FORM_DATA_VALUE + ) + String upload8 (@RequestPart("pojo") Pojo pojo, @RequestPart("files") List files); + + @RequestMapping( + path = "/multipart/upload9", + method = POST, + consumes = MULTIPART_FORM_DATA_VALUE + ) + String upload9Array (@RequestPart("pojos") Pojo[] pojos, @RequestPart("files") List files); + + @RequestMapping( + path = "/multipart/upload9", + method = POST, + consumes = MULTIPART_FORM_DATA_VALUE + ) + String upload9Collection (@RequestPart("pojos") List pojos, @RequestPart("files") List files); class ClientConfiguration { @@ -115,7 +147,21 @@ class ClientConfiguration { @Bean public Encoder feignEncoder () { - return new SpringFormEncoder(new SpringEncoder(messageConverters)); + PojoSerializationWriter pojoSerializationWriter = new PojoSerializationWriter() { + private ObjectMapper objectMapper = new ObjectMapper(); + + @Override + protected MediaType getContentType() { + return MediaType.APPLICATION_JSON; + } + + @Override + protected String serialize(Object object) throws IOException { + return objectMapper.writeValueAsString(object); + } + }; + + return new SpringFormEncoder(pojoSerializationWriter, new SpringEncoder(messageConverters)); } @Bean diff --git a/feign-form-spring/src/test/java/feign/form/feign/spring/Pojo.java b/feign-form-spring/src/test/java/feign/form/feign/spring/Pojo.java new file mode 100644 index 0000000..f0668c3 --- /dev/null +++ b/feign-form-spring/src/test/java/feign/form/feign/spring/Pojo.java @@ -0,0 +1,36 @@ +/* + * Copyright 2020 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 feign.form.feign.spring; + +import static lombok.AccessLevel.PRIVATE; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.experimental.FieldDefaults; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@FieldDefaults(level = PRIVATE) +public class Pojo { + String field1; + + String field2; + + int field3; +} diff --git a/feign-form-spring/src/test/java/feign/form/feign/spring/Server.java b/feign-form-spring/src/test/java/feign/form/feign/spring/Server.java index 953a0b1..584be6e 100644 --- a/feign-form-spring/src/test/java/feign/form/feign/spring/Server.java +++ b/feign-form-spring/src/test/java/feign/form/feign/spring/Server.java @@ -25,6 +25,7 @@ import static org.springframework.web.bind.annotation.RequestMethod.POST; import java.io.IOException; +import java.util.List; import java.util.Map; import lombok.SneakyThrows; @@ -121,9 +122,11 @@ void upload5 (Dto dto) throws IOException { method = POST, consumes = MULTIPART_FORM_DATA_VALUE ) - public ResponseEntity upload6 (@RequestParam("popa1") MultipartFile popa1, - @RequestParam("popa2") MultipartFile popa2 + public ResponseEntity upload6 (@RequestPart("files") List files ) throws Exception { + MultipartFile popa1 = files.get(0); + MultipartFile popa2 = files.get(1); + HttpStatus status = I_AM_A_TEAPOT; String result = ""; if (popa1 != null && popa2 != null) { @@ -133,6 +136,48 @@ public ResponseEntity upload6 (@RequestParam("popa1") MultipartFile popa return ResponseEntity.status(status).body(result); } + @RequestMapping( + path = "/multipart/upload7", + method = POST, + consumes = MULTIPART_FORM_DATA_VALUE + ) + public ResponseEntity upload7 (@RequestPart("pojo") Pojo pojo + ) throws Exception { + val result = pojo.getField1() + pojo.getField2() + pojo.getField3(); + + return ResponseEntity.ok(result); + } + + @RequestMapping( + path = "/multipart/upload8", + method = POST, + consumes = MULTIPART_FORM_DATA_VALUE + ) + public ResponseEntity upload8 (@RequestPart("pojo") Pojo pojo, @RequestPart("files") List files + ) throws Exception { + val result1 = pojo.getField1() + pojo.getField2() + pojo.getField3(); + val result2 = new String(files.get(0).getBytes()) + new String(files.get(1).getBytes()); + + return ResponseEntity.ok(result1 + result2); + } + + @RequestMapping( + path = "/multipart/upload9", + method = POST, + consumes = MULTIPART_FORM_DATA_VALUE + ) + public ResponseEntity upload9 (@RequestPart("pojos") List pojos, @RequestPart("files") List files + ) throws Exception { + val pojo1 = pojos.get(0); + val pojo2 = pojos.get(1); + + val result1 = pojo1.getField1() + pojo1.getField2() + pojo1.getField3(); + val result2 = pojo2.getField1() + pojo2.getField2() + pojo2.getField3(); + val result3 = new String(files.get(0).getBytes()) + new String(files.get(1).getBytes()); + + return ResponseEntity.ok(result1 + result2 + result3); + } + @RequestMapping( path = "/multipart/download/{fileId}", method = GET, diff --git a/feign-form-spring/src/test/java/feign/form/feign/spring/SpringFormEncoderTest.java b/feign-form-spring/src/test/java/feign/form/feign/spring/SpringFormEncoderTest.java index 1736cfa..3fbb886 100644 --- a/feign-form-spring/src/test/java/feign/form/feign/spring/SpringFormEncoderTest.java +++ b/feign-form-spring/src/test/java/feign/form/feign/spring/SpringFormEncoderTest.java @@ -125,7 +125,7 @@ public void upload6ArrayTest () throws Exception { @Test public void upload6CollectionTest () throws Exception { - List list = asList( + val list = asList( (MultipartFile) new MockMultipartFile("popa1", "popa1", null, "Hello".getBytes(UTF_8)), (MultipartFile) new MockMultipartFile("popa2", "popa2", null, " world".getBytes(UTF_8)) ); @@ -133,4 +133,77 @@ public void upload6CollectionTest () throws Exception { val response = client.upload6Collection(list); Assert.assertEquals("Hello world", response); } + + @Test + public void upload6ArraySameNameTest () throws Exception { + val file1 = new MockMultipartFile("popa0", "popa1", null, "Hello".getBytes(UTF_8)); + val file2 = new MockMultipartFile("popa0", "popa2", null, " world".getBytes(UTF_8)); + + val response = client.upload6Array(new MultipartFile[] { file1, file2 }); + Assert.assertEquals("Hello world", response); + } + + @Test + public void upload6CollectionSameNameTest () throws Exception { + List list = asList( + (MultipartFile) new MockMultipartFile("popa0", "popa1", null, "Hello".getBytes(UTF_8)), + (MultipartFile) new MockMultipartFile("popa0", "popa2", null, " world".getBytes(UTF_8)) + ); + + val response = client.upload6Collection(list); + Assert.assertEquals("Hello world", response); + } + + @Test + public void upload7Test () throws Exception { + val pojo = new Pojo("Hello", " world", 1); + + val response = client.upload7(pojo); + Assert.assertEquals("Hello world1", response); + } + + @Test + public void upload8Test () throws Exception { + val pojo = new Pojo("Hello", " world", 1); + + val list = asList( + (MultipartFile) new MockMultipartFile("files", "popa1", null, "Hello".getBytes(UTF_8)), + (MultipartFile) new MockMultipartFile("files", "popa2", null, " world".getBytes(UTF_8)) + ); + + val response = client.upload8(pojo, list); + Assert.assertEquals("Hello world1Hello world", response); + } + + @Test + public void upload9ArrayTest () throws Exception { + val pojos = new Pojo[]{ + new Pojo("Hello", " world", 1), + new Pojo("Hello", " world", 2) + }; + + val list = asList( + (MultipartFile) new MockMultipartFile("files", "popa1", null, "Hello".getBytes(UTF_8)), + (MultipartFile) new MockMultipartFile("files", "popa2", null, " world".getBytes(UTF_8)) + ); + + val response = client.upload9Array(pojos, list); + Assert.assertEquals("Hello world1Hello world2Hello world", response); + } + + @Test + public void upload9CollectionTest () throws Exception { + val pojos = asList( + new Pojo("Hello", " world", 1), + new Pojo("Hello", " world", 2) + ); + + val list = asList( + (MultipartFile) new MockMultipartFile("files", "popa1", null, "Hello".getBytes(UTF_8)), + (MultipartFile) new MockMultipartFile("files", "popa2", null, " world".getBytes(UTF_8)) + ); + + val response = client.upload9Collection(pojos, list); + Assert.assertEquals("Hello world1Hello world2Hello world", response); + } } diff --git a/pom.xml b/pom.xml index d038562..9a40ef1 100644 --- a/pom.xml +++ b/pom.xml @@ -146,13 +146,13 @@ limitations under the License. org.springframework.boot spring-boot-starter-web - 2.1.3.RELEASE + 2.2.6.RELEASE test org.springframework.boot spring-boot-starter-test - 2.1.3.RELEASE + 2.2.6.RELEASE test