Skip to content

Commit 59ba6b1

Browse files
authored
Allow @RequestPart user-defined POJOs (#314)
* Add Pojo and failing test to FeignClientTests * Add SpringPojoFormEncoder * Fix configuration * Update Pojo * Implement multipartPojo * Does not work if Pojo has one field: this is crazy * Disable expansion for multipart/form-data * Remove Pojo * Tidy code * Add testSinglePojoRequestPart * Update testMultiplePojoRequestPart * Update testMultiplePojoRequestPart * Add testRequestPartWithListOfPojosAndListOfMultipartFiles * Fix Checkstyle errors * Update license year and authors * Tidy isApplicable * Refactor isApplicable * Rename classes and methods * Change variable names * Rename PojoFormWriter to AbstractFormWriter * Rename method * Use ObjectProvider * Change constructor of SpringEncoder to move SpringPojoFormEncoder to FeignClientsConfiguration * Rename variables * Remove unused variable * Extract springEncoder() method * Restore previous constructor * Extract isMultipartFormData() method
1 parent dd91dcd commit 59ba6b1

File tree

6 files changed

+294
-11
lines changed

6 files changed

+294
-11
lines changed

spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsConfiguration.java

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013-2019 the original author or authors.
2+
* Copyright 2013-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -27,17 +27,21 @@
2727
import feign.Retryer;
2828
import feign.codec.Decoder;
2929
import feign.codec.Encoder;
30+
import feign.form.MultipartFormContentProcessor;
31+
import feign.form.spring.SpringFormEncoder;
3032
import feign.hystrix.HystrixFeign;
3133
import feign.optionals.OptionalDecoder;
3234

3335
import org.springframework.beans.factory.ObjectFactory;
36+
import org.springframework.beans.factory.ObjectProvider;
3437
import org.springframework.beans.factory.annotation.Autowired;
3538
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
3639
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
3740
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
3841
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
3942
import org.springframework.boot.autoconfigure.data.web.SpringDataWebProperties;
4043
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
44+
import org.springframework.cloud.openfeign.support.AbstractFormWriter;
4145
import org.springframework.cloud.openfeign.support.PageJacksonModule;
4246
import org.springframework.cloud.openfeign.support.PageableSpringEncoder;
4347
import org.springframework.cloud.openfeign.support.ResponseEntityDecoder;
@@ -51,9 +55,12 @@
5155
import org.springframework.format.support.DefaultFormattingConversionService;
5256
import org.springframework.format.support.FormattingConversionService;
5357

58+
import static feign.form.ContentType.MULTIPART;
59+
5460
/**
5561
* @author Dave Syer
5662
* @author Venil Noronha
63+
* @author Darren Foong
5764
*/
5865
@Configuration(proxyBeanMethods = false)
5966
public class FeignClientsConfiguration {
@@ -83,16 +90,18 @@ public Decoder feignDecoder() {
8390
@Bean
8491
@ConditionalOnMissingBean
8592
@ConditionalOnMissingClass("org.springframework.data.domain.Pageable")
86-
public Encoder feignEncoder() {
87-
return new SpringEncoder(this.messageConverters);
93+
public Encoder feignEncoder(ObjectProvider<AbstractFormWriter> formWriterProvider) {
94+
return springEncoder(formWriterProvider);
8895
}
8996

9097
@Bean
9198
@ConditionalOnClass(name = "org.springframework.data.domain.Pageable")
9299
@ConditionalOnMissingBean
93-
public Encoder feignEncoderPageable() {
100+
public Encoder feignEncoderPageable(
101+
ObjectProvider<AbstractFormWriter> formWriterProvider) {
94102
PageableSpringEncoder encoder = new PageableSpringEncoder(
95-
new SpringEncoder(this.messageConverters));
103+
springEncoder(formWriterProvider));
104+
96105
if (springDataWebProperties != null) {
97106
encoder.setPageParameter(
98107
springDataWebProperties.getPageable().getPageParameter());
@@ -144,6 +153,18 @@ public Module pageJacksonModule() {
144153
return new PageJacksonModule();
145154
}
146155

156+
private Encoder springEncoder(ObjectProvider<AbstractFormWriter> formWriterProvider) {
157+
AbstractFormWriter formWriter = formWriterProvider.getIfAvailable();
158+
159+
if (formWriter != null) {
160+
return new SpringEncoder(new SpringPojoFormEncoder(formWriter),
161+
this.messageConverters);
162+
}
163+
else {
164+
return new SpringEncoder(new SpringFormEncoder(), this.messageConverters);
165+
}
166+
}
167+
147168
@Configuration(proxyBeanMethods = false)
148169
@ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class })
149170
protected static class HystrixFeignConfiguration {
@@ -158,4 +179,16 @@ public Feign.Builder feignHystrixBuilder() {
158179

159180
}
160181

182+
private class SpringPojoFormEncoder extends SpringFormEncoder {
183+
184+
SpringPojoFormEncoder(AbstractFormWriter formWriter) {
185+
super();
186+
187+
MultipartFormContentProcessor processor = (MultipartFormContentProcessor) getContentProcessor(
188+
MULTIPART);
189+
processor.addFirstWriter(formWriter);
190+
}
191+
192+
}
193+
161194
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
/*
2+
* Copyright 2013-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.openfeign.support;
18+
19+
import java.io.IOException;
20+
import java.util.Iterator;
21+
import java.util.function.Predicate;
22+
23+
import feign.codec.EncodeException;
24+
import feign.form.multipart.AbstractWriter;
25+
import feign.form.multipart.Output;
26+
import feign.form.util.PojoUtil;
27+
28+
import org.springframework.http.MediaType;
29+
import org.springframework.web.multipart.MultipartFile;
30+
31+
import static feign.form.ContentProcessor.CRLF;
32+
import static feign.form.util.PojoUtil.isUserPojo;
33+
34+
/**
35+
* @author Darren Foong
36+
*/
37+
public abstract class AbstractFormWriter extends AbstractWriter {
38+
39+
@Override
40+
public boolean isApplicable(Object object) {
41+
return !isTypeOrCollection(object, o -> o instanceof MultipartFile)
42+
&& isTypeOrCollection(object, PojoUtil::isUserPojo);
43+
}
44+
45+
@Override
46+
public void write(Output output, String key, Object object) throws EncodeException {
47+
try {
48+
String string = new StringBuilder()
49+
.append("Content-Disposition: form-data; name=\"").append(key)
50+
.append('"').append(CRLF).append("Content-Type: ")
51+
.append(getContentType()).append("; charset=")
52+
.append(output.getCharset().name()).append(CRLF).append(CRLF)
53+
.append(writeAsString(object)).toString();
54+
55+
output.write(string);
56+
}
57+
catch (IOException e) {
58+
throw new EncodeException(e.getMessage());
59+
}
60+
}
61+
62+
protected abstract MediaType getContentType();
63+
64+
protected abstract String writeAsString(Object object) throws IOException;
65+
66+
private boolean isTypeOrCollection(Object object, Predicate<Object> isType) {
67+
if (object.getClass().isArray()) {
68+
Object[] array = (Object[]) object;
69+
70+
return array.length > 1 && isType.test(array[0]);
71+
}
72+
else if (object instanceof Iterable) {
73+
Iterable<?> iterable = (Iterable<?>) object;
74+
Iterator<?> iterator = iterable.iterator();
75+
76+
return iterator.hasNext() && isType.test(iterator.next());
77+
}
78+
else {
79+
return isType.test(object);
80+
}
81+
}
82+
83+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2013-2020 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.cloud.openfeign.support;
18+
19+
import java.io.IOException;
20+
21+
import com.fasterxml.jackson.databind.ObjectMapper;
22+
23+
import org.springframework.beans.factory.annotation.Autowired;
24+
import org.springframework.http.MediaType;
25+
import org.springframework.stereotype.Component;
26+
27+
/**
28+
* @author Darren Foong
29+
*/
30+
@Component
31+
public class JsonFormWriter extends AbstractFormWriter {
32+
33+
@Autowired
34+
private ObjectMapper objectMapper;
35+
36+
@Override
37+
protected MediaType getContentType() {
38+
return MediaType.APPLICATION_JSON;
39+
}
40+
41+
@Override
42+
protected String writeAsString(Object object) throws IOException {
43+
return objectMapper.writeValueAsString(object);
44+
}
45+
46+
}

spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringEncoder.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013-2019 the original author or authors.
2+
* Copyright 2013-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -54,16 +54,24 @@
5454
* @author Scien Jus
5555
* @author Ahmad Mozafarnia
5656
* @author Aaron Whiteside
57+
* @author Darren Foong
5758
*/
5859
public class SpringEncoder implements Encoder {
5960

6061
private static final Log log = LogFactory.getLog(SpringEncoder.class);
6162

62-
private final SpringFormEncoder springFormEncoder = new SpringFormEncoder();
63+
private final SpringFormEncoder springFormEncoder;
6364

6465
private final ObjectFactory<HttpMessageConverters> messageConverters;
6566

6667
public SpringEncoder(ObjectFactory<HttpMessageConverters> messageConverters) {
68+
this.springFormEncoder = new SpringFormEncoder();
69+
this.messageConverters = messageConverters;
70+
}
71+
72+
public SpringEncoder(SpringFormEncoder springFormEncoder,
73+
ObjectFactory<HttpMessageConverters> messageConverters) {
74+
this.springFormEncoder = springFormEncoder;
6775
this.messageConverters = messageConverters;
6876
}
6977

spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/support/SpringMvcContract.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013-2019 the original author or authors.
2+
* Copyright 2013-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -28,6 +28,7 @@
2828
import java.util.LinkedHashMap;
2929
import java.util.List;
3030
import java.util.Map;
31+
import java.util.Objects;
3132

3233
import feign.Contract;
3334
import feign.Feign;
@@ -42,6 +43,7 @@
4243
import org.springframework.cloud.openfeign.annotation.RequestHeaderParameterProcessor;
4344
import org.springframework.cloud.openfeign.annotation.RequestParamParameterProcessor;
4445
import org.springframework.cloud.openfeign.annotation.RequestPartParameterProcessor;
46+
import org.springframework.cloud.openfeign.encoding.HttpEncoding;
4547
import org.springframework.context.ConfigurableApplicationContext;
4648
import org.springframework.context.ResourceLoaderAware;
4749
import org.springframework.core.DefaultParameterNameDiscoverer;
@@ -54,6 +56,7 @@
5456
import org.springframework.core.convert.support.DefaultConversionService;
5557
import org.springframework.core.io.DefaultResourceLoader;
5658
import org.springframework.core.io.ResourceLoader;
59+
import org.springframework.http.MediaType;
5760
import org.springframework.util.Assert;
5861
import org.springframework.util.StringUtils;
5962
import org.springframework.web.bind.annotation.RequestMapping;
@@ -72,6 +75,7 @@
7275
* @author Olga Maciaszek-Sharma
7376
* @author Aaron Whiteside
7477
* @author Artyom Romanenko
78+
* @author Darren Foong
7579
*/
7680
public class SpringMvcContract extends Contract.BaseContract
7781
implements ResourceLoaderAware {
@@ -291,7 +295,8 @@ protected boolean processAnnotationsOnParameter(MethodMetadata data,
291295
}
292296
}
293297

294-
if (isHttpAnnotation && data.indexToExpander().get(paramIndex) == null) {
298+
if (!isMultipartFormData(data) && isHttpAnnotation
299+
&& data.indexToExpander().get(paramIndex) == null) {
295300
TypeDescriptor typeDescriptor = createTypeDescriptor(method, paramIndex);
296301
if (this.conversionService.canConvert(typeDescriptor,
297302
STRING_TYPE_DESCRIPTOR)) {
@@ -388,6 +393,18 @@ private boolean shouldAddParameterName(int parameterIndex, Type[] parameterTypes
388393
&& parameterTypes != null && parameterTypes.length > parameterIndex;
389394
}
390395

396+
private boolean isMultipartFormData(MethodMetadata data) {
397+
Collection<String> contentTypes = data.template().headers()
398+
.get(HttpEncoding.CONTENT_TYPE);
399+
400+
if (contentTypes != null && !contentTypes.isEmpty()) {
401+
String type = contentTypes.iterator().next();
402+
return Objects.equals(MediaType.valueOf(type), MediaType.MULTIPART_FORM_DATA);
403+
}
404+
405+
return false;
406+
}
407+
391408
/**
392409
* @deprecated Not used internally anymore. Will be removed in the future.
393410
*/

0 commit comments

Comments
 (0)