Skip to content

Commit 735850a

Browse files
authored
Merge pull request #3286 from yvasyliev/feature/form-urlencoded-collection-format
feat(form): enhance `UrlencodedFormContentProcessor` to support `CollectionFormats` in key-value pairs
2 parents c0541d9 + 161c424 commit 735850a

3 files changed

Lines changed: 184 additions & 27 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
### Version 13.12
2+
3+
* `UrlencodedFormContentProcessor` now honors `CollectionFormat` from `@RequestLine`/`RequestTemplate` for array and
4+
collection values in `application/x-www-form-urlencoded` bodies.
5+
16
### Version 11.9
27

38
* `OkHttpClient` now implements `AsyncClient`

form/src/main/java/feign/form/UrlencodedFormContentProcessor.java

Lines changed: 17 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,19 @@
1717

1818
import static feign.form.ContentType.URLENCODED;
1919

20+
import feign.CollectionFormat;
2021
import feign.RequestTemplate;
2122
import feign.codec.EncodeException;
2223
import java.net.URLEncoder;
2324
import java.nio.charset.Charset;
25+
import java.util.Arrays;
2426
import java.util.Collection;
2527
import java.util.Collections;
2628
import java.util.Map;
2729
import java.util.Map.Entry;
30+
import java.util.Objects;
31+
import java.util.stream.Collectors;
32+
import java.util.stream.Stream;
2833
import lombok.SneakyThrows;
2934
import lombok.val;
3035

@@ -55,7 +60,7 @@ public void process(RequestTemplate template, Charset charset, Map<String, Objec
5560
if (bodyData.length() > 0) {
5661
bodyData.append(QUERY_DELIMITER);
5762
}
58-
bodyData.append(createKeyValuePair(entry, charset));
63+
bodyData.append(createKeyValuePair(template.collectionFormat(), entry, charset));
5964
}
6065

6166
val contentTypeValue =
@@ -77,16 +82,19 @@ public ContentType getSupportedContentType() {
7782
return URLENCODED;
7883
}
7984

80-
private String createKeyValuePair(Entry<String, Object> entry, Charset charset) {
85+
private CharSequence createKeyValuePair(
86+
CollectionFormat collectionFormat, Entry<String, Object> entry, Charset charset) {
8187
String encodedKey = encode(entry.getKey(), charset);
8288
Object value = entry.getValue();
8389

8490
if (value == null) {
8591
return encodedKey;
8692
} else if (value.getClass().isArray()) {
87-
return createKeyValuePairFromArray(encodedKey, value, charset);
93+
return createKeyValuePair(
94+
collectionFormat, encodedKey, Arrays.stream((Object[]) value), charset);
8895
} else if (value instanceof Collection) {
89-
return createKeyValuePairFromCollection(encodedKey, value, charset);
96+
return createKeyValuePair(
97+
collectionFormat, encodedKey, ((Collection<?>) value).stream(), charset);
9098
}
9199
return new StringBuilder()
92100
.append(encodedKey)
@@ -95,28 +103,10 @@ private String createKeyValuePair(Entry<String, Object> entry, Charset charset)
95103
.toString();
96104
}
97105

98-
private String createKeyValuePairFromCollection(String key, Object values, Charset charset) {
99-
val collection = (Collection<?>) values;
100-
val array = collection.toArray(new Object[0]);
101-
return createKeyValuePairFromArray(key, array, charset);
102-
}
103-
104-
private String createKeyValuePairFromArray(String key, Object values, Charset charset) {
105-
val result = new StringBuilder();
106-
val array = (Object[]) values;
107-
108-
for (int index = 0; index < array.length; index++) {
109-
val value = array[index];
110-
if (value == null) {
111-
continue;
112-
}
113-
114-
if (index > 0) {
115-
result.append(QUERY_DELIMITER);
116-
}
117-
118-
result.append(key).append(EQUAL_SIGN).append(encode(value, charset));
119-
}
120-
return result.toString();
106+
private CharSequence createKeyValuePair(
107+
CollectionFormat collectionFormat, String key, Stream<?> values, Charset charset) {
108+
val stringValues =
109+
values.filter(Objects::nonNull).map(Object::toString).collect(Collectors.toList());
110+
return collectionFormat.join(key, stringValues, charset);
121111
}
122112
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*
2+
* Copyright © 2012 The Feign Authors (feign@commonhaus.dev)
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+
* http://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+
package feign.form;
17+
18+
import static org.assertj.core.api.Assertions.assertThat;
19+
20+
import feign.CollectionFormat;
21+
import feign.Feign;
22+
import feign.Headers;
23+
import feign.RequestLine;
24+
import feign.form.utils.UndertowServer;
25+
import feign.jackson.JacksonEncoder;
26+
import io.undertow.server.HttpServerExchange;
27+
import java.util.Arrays;
28+
import java.util.LinkedHashMap;
29+
import java.util.Map;
30+
import java.util.function.BiFunction;
31+
import org.junit.jupiter.api.Test;
32+
33+
class UrlencodedFormContentProcessorTest {
34+
35+
@Test
36+
void arrayValueUsesDefaultExplodedCollectionFormat() {
37+
assertEncodedBody(
38+
"from=%2B987654321&to=%2B123456789&tags=one&tags=two",
39+
new String[] {"one", "two"}, Client::map);
40+
}
41+
42+
@Test
43+
void collectionValueUsesDefaultExplodedCollectionFormat() {
44+
assertEncodedBody(
45+
"from=%2B987654321&to=%2B123456789&tags=one&tags=two",
46+
Arrays.asList("one", "two"), Client::map);
47+
}
48+
49+
@Test
50+
void arrayValueUsesCsvCollectionFormat() {
51+
assertEncodedBody(
52+
"from=%2B987654321&to=%2B123456789&tags=one%2Ctwo",
53+
new String[] {"one", "two"}, Client::mapCsv);
54+
}
55+
56+
@Test
57+
void collectionValueUsesCsvCollectionFormat() {
58+
assertEncodedBody(
59+
"from=%2B987654321&to=%2B123456789&tags=one%2Ctwo",
60+
Arrays.asList("one", "two"), Client::mapCsv);
61+
}
62+
63+
@Test
64+
void arrayValueUsesSsvCollectionFormat() {
65+
assertEncodedBody(
66+
"from=%2B987654321&to=%2B123456789&tags=one%20two",
67+
new String[] {"one", "two"}, Client::mapSsv);
68+
}
69+
70+
@Test
71+
void collectionValueUsesSsvCollectionFormat() {
72+
assertEncodedBody(
73+
"from=%2B987654321&to=%2B123456789&tags=one%20two",
74+
Arrays.asList("one", "two"), Client::mapSsv);
75+
}
76+
77+
@Test
78+
void arrayValueUsesTsvCollectionFormat() {
79+
assertEncodedBody(
80+
"from=%2B987654321&to=%2B123456789&tags=one%09two",
81+
new String[] {"one", "two"}, Client::mapTsv);
82+
}
83+
84+
@Test
85+
void collectionValueUsesTsvCollectionFormat() {
86+
assertEncodedBody(
87+
"from=%2B987654321&to=%2B123456789&tags=one%09two",
88+
Arrays.asList("one", "two"), Client::mapTsv);
89+
}
90+
91+
@Test
92+
void arrayValueUsesPipesCollectionFormat() {
93+
assertEncodedBody(
94+
"from=%2B987654321&to=%2B123456789&tags=one%7Ctwo",
95+
new String[] {"one", "two"}, Client::mapPipes);
96+
}
97+
98+
@Test
99+
void collectionValueUsesPipesCollectionFormat() {
100+
assertEncodedBody(
101+
"from=%2B987654321&to=%2B123456789&tags=one%7Ctwo",
102+
Arrays.asList("one", "two"), Client::mapPipes);
103+
}
104+
105+
private void assertEncodedBody(
106+
String expectedBody,
107+
Object tags,
108+
BiFunction<Client, Map<String, Object>, String> requestCall) {
109+
try (var server =
110+
UndertowServer.builder()
111+
.callback((exchange, message) -> assertRequest(exchange, message, expectedBody))
112+
.start()) {
113+
var client =
114+
Feign.builder()
115+
.encoder(new FormEncoder(new JacksonEncoder()))
116+
.target(Client.class, server.getConnectUrl());
117+
118+
var data = createRequestData(tags);
119+
assertThat(requestCall.apply(client, data)).isEqualTo("ok");
120+
}
121+
}
122+
123+
private Map<String, Object> createRequestData(Object tags) {
124+
var data = new LinkedHashMap<String, Object>();
125+
data.put("from", "+987654321");
126+
data.put("to", "+123456789");
127+
data.put("tags", tags);
128+
return data;
129+
}
130+
131+
private void assertRequest(HttpServerExchange exchange, byte[] message, String expectedBody) {
132+
assertThat(exchange.getRequestHeaders().getFirst(io.undertow.util.Headers.CONTENT_TYPE))
133+
.isEqualTo("application/x-www-form-urlencoded; charset=UTF-8");
134+
assertThat(message).asString().isEqualTo(expectedBody);
135+
136+
exchange.getResponseHeaders().put(io.undertow.util.Headers.CONTENT_TYPE, "text/plain");
137+
exchange.getResponseSender().send("ok");
138+
}
139+
140+
interface Client {
141+
142+
@RequestLine("POST")
143+
@Headers("Content-Type: application/x-www-form-urlencoded; charset=utf-8")
144+
String map(Map<String, Object> data);
145+
146+
@RequestLine(value = "POST", collectionFormat = CollectionFormat.CSV)
147+
@Headers("Content-Type: application/x-www-form-urlencoded; charset=utf-8")
148+
String mapCsv(Map<String, Object> data);
149+
150+
@RequestLine(value = "POST", collectionFormat = CollectionFormat.SSV)
151+
@Headers("Content-Type: application/x-www-form-urlencoded; charset=utf-8")
152+
String mapSsv(Map<String, Object> data);
153+
154+
@RequestLine(value = "POST", collectionFormat = CollectionFormat.TSV)
155+
@Headers("Content-Type: application/x-www-form-urlencoded; charset=utf-8")
156+
String mapTsv(Map<String, Object> data);
157+
158+
@RequestLine(value = "POST", collectionFormat = CollectionFormat.PIPES)
159+
@Headers("Content-Type: application/x-www-form-urlencoded; charset=utf-8")
160+
String mapPipes(Map<String, Object> data);
161+
}
162+
}

0 commit comments

Comments
 (0)