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..e100f20 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,20 +20,21 @@ import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE; import static org.springframework.web.bind.annotation.RequestMethod.POST; -import java.util.List; -import java.util.Map; - import feign.Logger; import feign.Response; import feign.codec.Encoder; import feign.form.spring.SpringFormEncoder; +import java.util.List; +import java.util.Map; import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.cloud.openfeign.support.SpringEncoder; import org.springframework.context.annotation.Bean; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -99,14 +100,20 @@ String upload4 (@PathVariable("id") String id, method = POST, consumes = MULTIPART_FORM_DATA_VALUE ) - String upload6Array (@RequestPart MultipartFile[] files); + String upload6Array(@RequestPart MultipartFile[] files); @RequestMapping( path = "/multipart/upload6", method = POST, consumes = MULTIPART_FORM_DATA_VALUE ) - String upload6Collection (@RequestPart List files); + String upload6Collection(@RequestPart List files); + + @PostMapping( + path = "/multipart/upload7", + consumes = MULTIPART_FORM_DATA_VALUE + ) + String upload7(@ModelAttribute SubDto dto); class ClientConfiguration { @@ -114,7 +121,7 @@ class ClientConfiguration { private ObjectFactory messageConverters; @Bean - public Encoder feignEncoder () { + public Encoder feignEncoder() { return new SpringFormEncoder(new SpringEncoder(messageConverters)); } diff --git a/feign-form-spring/src/test/java/feign/form/feign/spring/Dto.java b/feign-form-spring/src/test/java/feign/form/feign/spring/Dto.java index 2e812db..c9d3067 100644 --- a/feign-form-spring/src/test/java/feign/form/feign/spring/Dto.java +++ b/feign-form-spring/src/test/java/feign/form/feign/spring/Dto.java @@ -19,16 +19,17 @@ import static lombok.AccessLevel.PRIVATE; import java.io.Serializable; - import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; +import lombok.experimental.Accessors; import lombok.experimental.FieldDefaults; import org.springframework.web.multipart.MultipartFile; @Data @NoArgsConstructor @AllArgsConstructor +@Accessors(chain = true) @FieldDefaults(level = PRIVATE) public class Dto implements Serializable { 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..e64da95 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 @@ -24,9 +24,9 @@ import static org.springframework.web.bind.annotation.RequestMethod.GET; import static org.springframework.web.bind.annotation.RequestMethod.POST; +import feign.form.feign.spring.SubDto.SubEnumeration; import java.io.IOException; import java.util.Map; - import lombok.SneakyThrows; import lombok.val; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -39,7 +39,9 @@ import org.springframework.http.ResponseEntity; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; @@ -122,7 +124,7 @@ void upload5 (Dto dto) throws IOException { consumes = MULTIPART_FORM_DATA_VALUE ) public ResponseEntity upload6 (@RequestParam("popa1") MultipartFile popa1, - @RequestParam("popa2") MultipartFile popa2 + @RequestParam("popa2") MultipartFile popa2 ) throws Exception { HttpStatus status = I_AM_A_TEAPOT; String result = ""; @@ -133,12 +135,26 @@ public ResponseEntity upload6 (@RequestParam("popa1") MultipartFile popa return ResponseEntity.status(status).body(result); } + @PostMapping( + path = "/multipart/upload7", + consumes = MULTIPART_FORM_DATA_VALUE + ) + public ResponseEntity upload7(@ModelAttribute SubDto subDto) { + assert subDto != null; + assert subDto.getSomeEnum() != null; + if (subDto.getFile() == null || subDto.getSomeEnum() != SubEnumeration.THREE + || subDto.getField1() == null) { + return ResponseEntity.status(I_AM_A_TEAPOT).build(); + } + return ResponseEntity.status(OK).body(subDto.getSomeEnum().name()); + } + @RequestMapping( path = "/multipart/download/{fileId}", method = GET, produces = MULTIPART_FORM_DATA_VALUE ) - public MultiValueMap download (@PathVariable("fileId") String fileId) { + public MultiValueMap download(@PathVariable("fileId") String fileId) { val multiParts = new LinkedMultiValueMap(); val infoString = "The text for file ID " + fileId + ". Testing unicode €"; 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..e201296 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 @@ -21,9 +21,10 @@ import static org.junit.Assert.assertEquals; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.DEFINED_PORT; +import feign.form.feign.spring.SubDto.SubEnumeration; +import java.nio.charset.StandardCharsets; import java.util.HashMap; import java.util.List; - import lombok.val; import org.junit.Assert; import org.junit.Test; @@ -124,7 +125,7 @@ public void upload6ArrayTest () throws Exception { } @Test - public void upload6CollectionTest () throws Exception { + public void upload6CollectionTest() throws Exception { List list = asList( (MultipartFile) new MockMultipartFile("popa1", "popa1", null, "Hello".getBytes(UTF_8)), (MultipartFile) new MockMultipartFile("popa2", "popa2", null, " world".getBytes(UTF_8)) @@ -133,4 +134,21 @@ public void upload6CollectionTest () throws Exception { val response = client.upload6Collection(list); Assert.assertEquals("Hello world", response); } + + @Test + public void upload7Test() { + val file1 = new MockMultipartFile("one.txt", "One".getBytes(StandardCharsets.UTF_8)); + val file2 = new MockMultipartFile("two.txt", "Two".getBytes(StandardCharsets.UTF_8)); + + val dto = new SubDto(); + dto.setSomeEnum(SubEnumeration.THREE) + .setSubBool(null) + .setSubFile(file2) + .setFile(file1) + .setField1("Field 1") + .setField2(42); + + val response = client.upload7(dto); + Assert.assertEquals(dto.getSomeEnum().name(), response); + } } diff --git a/feign-form-spring/src/test/java/feign/form/feign/spring/SubDto.java b/feign-form-spring/src/test/java/feign/form/feign/spring/SubDto.java new file mode 100644 index 0000000..e7c8b0f --- /dev/null +++ b/feign-form-spring/src/test/java/feign/form/feign/spring/SubDto.java @@ -0,0 +1,32 @@ +package feign.form.feign.spring; + +import static lombok.AccessLevel.PRIVATE; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import lombok.ToString; +import lombok.experimental.Accessors; +import lombok.experimental.FieldDefaults; +import org.springframework.web.multipart.MultipartFile; + +@EqualsAndHashCode(callSuper = true) +@Data +@NoArgsConstructor +@AllArgsConstructor +@Accessors(chain = true) +@FieldDefaults(level = PRIVATE) +@ToString +public class SubDto extends Dto { + + MultipartFile subFile; + + SubEnumeration someEnum; + + Boolean subBool; + + public enum SubEnumeration { + ONE, TWO, THREE + } +} diff --git a/feign-form/src/main/java/feign/form/FormEncoder.java b/feign-form/src/main/java/feign/form/FormEncoder.java index 0bd5a9e..46e5259 100644 --- a/feign-form/src/main/java/feign/form/FormEncoder.java +++ b/feign-form/src/main/java/feign/form/FormEncoder.java @@ -22,16 +22,15 @@ import static java.util.Arrays.asList; import static lombok.AccessLevel.PRIVATE; +import feign.RequestTemplate; +import feign.codec.EncodeException; +import feign.codec.Encoder; import java.lang.reflect.Type; import java.nio.charset.Charset; import java.util.Collection; import java.util.HashMap; import java.util.Map; import java.util.regex.Pattern; - -import feign.RequestTemplate; -import feign.codec.EncodeException; -import feign.codec.Encoder; import lombok.experimental.FieldDefaults; import lombok.val; @@ -75,7 +74,7 @@ public FormEncoder (Encoder delegate) { new UrlencodedFormContentProcessor() ); - processors = new HashMap(list.size(), 1.F); + processors = new HashMap<>(list.size(), 1.F); for (ContentProcessor processor : list) { processors.put(processor.getSupportedContentType(), processor); } @@ -95,7 +94,7 @@ public void encode (Object object, Type bodyType, RequestTemplate template) thro if (MAP_STRING_WILDCARD.equals(bodyType)) { data = (Map) object; } else if (isUserPojo(bodyType)) { - data = toMap(object); + data = toMap(object, false, true); } else { delegate.encode(object, bodyType, template); return; diff --git a/feign-form/src/main/java/feign/form/multipart/PojoWriter.java b/feign-form/src/main/java/feign/form/multipart/PojoWriter.java index fe27fa7..709d63c 100644 --- a/feign-form/src/main/java/feign/form/multipart/PojoWriter.java +++ b/feign-form/src/main/java/feign/form/multipart/PojoWriter.java @@ -42,7 +42,7 @@ public boolean isApplicable (Object object) { @Override public void write (Output output, String boundary, String key, Object object) throws EncodeException { - val map = toMap(object); + val map = toMap(object, false, true); for (val entry : map.entrySet()) { val writer = findApplicableWriter(entry.getValue()); if (writer == null) { diff --git a/feign-form/src/main/java/feign/form/util/PojoUtil.java b/feign-form/src/main/java/feign/form/util/PojoUtil.java index e2b89a5..96fd249 100644 --- a/feign-form/src/main/java/feign/form/util/PojoUtil.java +++ b/feign-form/src/main/java/feign/form/util/PojoUtil.java @@ -20,24 +20,27 @@ import static java.lang.reflect.Modifier.isStatic; import static lombok.AccessLevel.PRIVATE; +import feign.codec.EncodeException; +import feign.form.FormProperty; import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.lang.reflect.Type; import java.rmi.UnexpectedException; import java.security.AccessController; import java.security.PrivilegedAction; +import java.util.AbstractMap; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; - +import java.util.stream.Collectors; import javax.annotation.Nullable; - -import feign.form.FormProperty; - import lombok.NoArgsConstructor; import lombok.NonNull; import lombok.Setter; import lombok.SneakyThrows; import lombok.experimental.FieldDefaults; import lombok.val; +import lombok.var; /** * @@ -57,6 +60,7 @@ public static boolean isUserPojo (@NonNull Type type) { } @SneakyThrows + @Deprecated public static Map toMap (@NonNull Object object) { val result = new HashMap(); val type = object.getClass(); @@ -75,15 +79,68 @@ public static Map toMap (@NonNull Object object) { } val propertyKey = field.isAnnotationPresent(FormProperty.class) - ? field.getAnnotation(FormProperty.class).value() - : field.getName(); + ? field.getAnnotation(FormProperty.class).value() + : field.getName(); result.put(propertyKey, fieldValue); } return result; } - private PojoUtil () throws UnexpectedException { + public static Map toMap( + final @NonNull Object object, + final boolean processTransient, + final boolean processFinal) { + final var result = new HashMap(); + var clazz = object.getClass(); + val setAccessibleAction = new SetAccessibleAction(); + while (clazz != null) { + final var fieldResult = Arrays.stream(clazz.getDeclaredFields()) + .filter(field -> + (processFinal || !Modifier.isFinal(field.getModifiers())) && + (processTransient || !Modifier.isTransient(field.getModifiers())) && + !Modifier.isStatic(field.getModifiers())) + .map(field -> toMapDoOnEach(setAccessibleAction, field, object)) + .filter(entry -> entry.getValue() != null) + .collect(Collectors.toMap( + Map.Entry::getKey, + Map.Entry::getValue, + (oldObj, newObj) -> newObj, + HashMap::new)); + result.putAll(fieldResult); + clazz = clazz.getSuperclass(); + } + return result; + } + + private static void setFieldAccessible( + final SetAccessibleAction setAccessibleAction, + final Field field) { + setAccessibleAction.setField(field); + AccessController.doPrivileged(setAccessibleAction); + } + + private static Map.Entry toMapDoOnEach( + final SetAccessibleAction setAccessibleAction, + final Field field, + final Object object) throws EncodeException { + + setFieldAccessible(setAccessibleAction, field); + try { + var fieldValue = field.get(object); + if (fieldValue != null && fieldValue.getClass().isEnum()) { + fieldValue = ((Enum) fieldValue).name(); + } + final var propertyKey = field.isAnnotationPresent(FormProperty.class) + ? field.getAnnotation(FormProperty.class).value() + : field.getName(); + return new AbstractMap.SimpleEntry<>(propertyKey, fieldValue); + } catch (Exception err) { + throw new EncodeException(err.getMessage(), err); + } + } + + private PojoUtil() throws UnexpectedException { throw new UnexpectedException("It is not allowed to instantiate this class"); } diff --git a/pom.xml b/pom.xml index d038562..21ea80c 100644 --- a/pom.xml +++ b/pom.xml @@ -36,11 +36,14 @@ limitations under the License. UTF-8 UTF-8 - 1.6 - 1.6 + 1.8 + 1.8 1.8 1.8 + + 1.8 + java18 Open Feign Forms Parent @@ -132,14 +135,14 @@ limitations under the License. io.github.openfeign feign-core - 10.2.0 + 11.6 provided io.github.openfeign feign-jackson - 10.2.0 + 11.6 test