Skip to content

Commit a69c2af

Browse files
kdavisk6velo
authored andcommitted
Adding Support for Query Parameter Name Expansion (#841)
* Adding Support for Query Parameter Name Expansion Fixes #838 `QueryTemplate` assumed that all query names were literals. This change adds support for Expressions in Query Parameter names, providing better adherence to RFC 6570. RequestLines such as `@RequestLine("GET /uri?{parameter}={value}")` are now fully expanded whereas before, only `{value}` would be. * Adding Encoding and Resolution Enums for Template Control These new enums replace the boolean values used to control encoding and expression expansion options in Templates * Allow unresolved expressions in Query Parameter Name and Body Template Expressions in Query Parameter names and in a Body Template will now no longer be removed if they are not resolved. For Query Template, this change will prevent invalid query name/value pairs from being generated. For the Body Template, the documentation states that unresolved should be preserved, yet the code did not match.
1 parent afe673e commit a69c2af

File tree

8 files changed

+93
-28
lines changed

8 files changed

+93
-28
lines changed

core/src/main/java/feign/template/BodyTemplate.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ public static BodyTemplate create(String template) {
3434
}
3535

3636
private BodyTemplate(String value, Charset charset) {
37-
super(value, false, false, false, charset);
37+
super(value, ExpansionOptions.ALLOW_UNRESOLVED, EncodingOptions.NOT_REQUIRED, false, charset);
3838
}
3939

4040
@Override

core/src/main/java/feign/template/Expression.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,4 +64,9 @@ public String getValue() {
6464
}
6565
return "{" + this.name + "}";
6666
}
67+
68+
@Override
69+
public String toString() {
70+
return this.getValue();
71+
}
6772
}

core/src/main/java/feign/template/HeaderTemplate.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public static HeaderTemplate append(HeaderTemplate headerTemplate, Iterable<Stri
7979
* @param template to parse.
8080
*/
8181
private HeaderTemplate(String template, String name, Iterable<String> values, Charset charset) {
82-
super(template, false, false, false, charset);
82+
super(template, ExpansionOptions.REQUIRED, EncodingOptions.NOT_REQUIRED, false, charset);
8383
this.values =
8484
StreamSupport.stream(values.spliterator(), false)
8585
.filter(Util::isNotBlank)

core/src/main/java/feign/template/QueryTemplate.java

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public final class QueryTemplate extends Template {
3030

3131
/* cache a copy of the variables for lookup later */
3232
private List<String> values;
33-
private final String name;
33+
private final Template name;
3434
private final CollectionFormat collectionFormat;
3535
private boolean pure = false;
3636

@@ -115,8 +115,10 @@ private QueryTemplate(
115115
Iterable<String> values,
116116
Charset charset,
117117
CollectionFormat collectionFormat) {
118-
super(template, false, true, true, charset);
119-
this.name = name;
118+
super(template, ExpansionOptions.REQUIRED, EncodingOptions.REQUIRED, true, charset);
119+
this.name =
120+
new Template(
121+
name, ExpansionOptions.ALLOW_UNRESOLVED, EncodingOptions.REQUIRED, false, charset);
120122
this.collectionFormat = collectionFormat;
121123
this.values =
122124
StreamSupport.stream(values.spliterator(), false)
@@ -133,12 +135,12 @@ public List<String> getValues() {
133135
}
134136

135137
public String getName() {
136-
return name;
138+
return name.toString();
137139
}
138140

139141
@Override
140142
public String toString() {
141-
return this.queryString(super.toString());
143+
return this.queryString(this.name.toString(), super.toString());
142144
}
143145

144146
/**
@@ -150,20 +152,21 @@ public String toString() {
150152
*/
151153
@Override
152154
public String expand(Map<String, ?> variables) {
153-
return this.queryString(super.expand(variables));
155+
String name = this.name.expand(variables);
156+
return this.queryString(name, super.expand(variables));
154157
}
155158

156-
private String queryString(String values) {
159+
private String queryString(String name, String values) {
157160
if (this.pure) {
158-
return this.name;
161+
return name;
159162
}
160163

161164
/* covert the comma separated values into a value query string */
162165
List<String> resolved =
163166
Arrays.stream(values.split(",")).filter(Util::isNotBlank).collect(Collectors.toList());
164167

165168
if (!resolved.isEmpty()) {
166-
return this.collectionFormat.join(this.name, resolved, this.getCharset()).toString();
169+
return this.collectionFormat.join(name, resolved, this.getCharset()).toString();
167170
}
168171

169172
/* nothing to return, all values are unresolved */

core/src/main/java/feign/template/Template.java

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,13 @@
2828
* href="https://tools.ietf.org/html/rfc6570">RFC 6570</a>, with some relaxed rules, allowing the
2929
* concept to be used in areas outside of the uri.
3030
*/
31-
public abstract class Template {
31+
public class Template {
3232

3333
private static final Logger logger = Logger.getLogger(Template.class.getName());
3434
private static final Pattern QUERY_STRING_PATTERN = Pattern.compile("(?<!\\{)(\\?)");
3535
private final String template;
3636
private final boolean allowUnresolved;
37-
private final boolean encode;
37+
private final EncodingOptions encode;
3838
private final boolean encodeSlash;
3939
private final Charset charset;
4040
private final List<TemplateChunk> templateChunks = new ArrayList<>();
@@ -48,12 +48,16 @@ public abstract class Template {
4848
* @param encodeSlash if slash characters should be encoded.
4949
*/
5050
Template(
51-
String value, boolean allowUnresolved, boolean encode, boolean encodeSlash, Charset charset) {
51+
String value,
52+
ExpansionOptions allowUnresolved,
53+
EncodingOptions encode,
54+
boolean encodeSlash,
55+
Charset charset) {
5256
if (value == null) {
5357
throw new IllegalArgumentException("template is required.");
5458
}
5559
this.template = value;
56-
this.allowUnresolved = allowUnresolved;
60+
this.allowUnresolved = ExpansionOptions.ALLOW_UNRESOLVED == allowUnresolved;
5761
this.encode = encode;
5862
this.encodeSlash = encodeSlash;
5963
this.charset = charset;
@@ -78,7 +82,7 @@ public String expand(Map<String, ?> variables) {
7882
Expression expression = (Expression) chunk;
7983
Object value = variables.get(expression.getName());
8084
if (value != null) {
81-
String expanded = expression.expand(value, this.encode);
85+
String expanded = expression.expand(value, this.encode.isEncodingRequired());
8286
if (!this.encodeSlash) {
8387
logger.fine("Explicit slash decoding specified, decoding all slashes in uri");
8488
expanded = expanded.replaceAll("\\%2F", "/");
@@ -105,7 +109,7 @@ public String expand(Map<String, ?> variables) {
105109
* @return the encoded value.
106110
*/
107111
private String encode(String value) {
108-
return this.encode ? UriUtils.encode(value, this.charset) : value;
112+
return this.encode.isEncodingRequired() ? UriUtils.encode(value, this.charset) : value;
109113
}
110114

111115
/**
@@ -116,7 +120,7 @@ private String encode(String value) {
116120
* @return the encoded value
117121
*/
118122
private String encode(String value, boolean query) {
119-
if (this.encode) {
123+
if (this.encode.isEncodingRequired()) {
120124
return query
121125
? UriUtils.queryEncode(value, this.charset)
122126
: UriUtils.pathEncode(value, this.charset);
@@ -216,15 +220,11 @@ public String toString() {
216220
return this.templateChunks.stream().map(TemplateChunk::getValue).collect(Collectors.joining());
217221
}
218222

219-
public boolean allowUnresolved() {
220-
return allowUnresolved;
221-
}
222-
223223
public boolean encode() {
224-
return encode;
224+
return encode.isEncodingRequired();
225225
}
226226

227-
public boolean encodeSlash() {
227+
boolean encodeSlash() {
228228
return encodeSlash;
229229
}
230230

@@ -312,4 +312,24 @@ public String next() {
312312
throw new IllegalStateException("No More Elements");
313313
}
314314
}
315+
316+
public enum EncodingOptions {
317+
REQUIRED(true),
318+
NOT_REQUIRED(false);
319+
320+
private boolean shouldEncode;
321+
322+
EncodingOptions(boolean shouldEncode) {
323+
this.shouldEncode = shouldEncode;
324+
}
325+
326+
public boolean isEncodingRequired() {
327+
return this.shouldEncode;
328+
}
329+
}
330+
331+
public enum ExpansionOptions {
332+
ALLOW_UNRESOLVED,
333+
REQUIRED
334+
}
315335
}

core/src/main/java/feign/template/UriTemplate.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,6 @@ public static UriTemplate append(UriTemplate uriTemplate, String fragment) {
7070
* @param charset to use when encoding.
7171
*/
7272
private UriTemplate(String template, boolean encodeSlash, Charset charset) {
73-
super(template, false, true, encodeSlash, charset);
73+
super(template, ExpansionOptions.REQUIRED, EncodingOptions.REQUIRED, encodeSlash, charset);
7474
}
7575
}

core/src/test/java/feign/DefaultContractTest.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,9 @@ public void headersOnMethodAddsContentTypeHeader() throws Exception {
132132
assertThat(md.template())
133133
.hasHeaders(
134134
entry("Content-Type", asList("application/xml")),
135-
entry("Content-Length", asList(String.valueOf(md.template().body().length))));
135+
entry(
136+
"Content-Length",
137+
asList(String.valueOf(md.template().requestBody().asBytes().length))));
136138
}
137139

138140
@Test
@@ -142,7 +144,9 @@ public void headersOnTypeAddsContentTypeHeader() throws Exception {
142144
assertThat(md.template())
143145
.hasHeaders(
144146
entry("Content-Type", asList("application/xml")),
145-
entry("Content-Length", asList(String.valueOf(md.template().body().length))));
147+
entry(
148+
"Content-Length",
149+
asList(String.valueOf(md.template().requestBody().asBytes().length))));
146150
}
147151

148152
@Test
@@ -152,7 +156,9 @@ public void headersContainsWhitespaces() throws Exception {
152156
assertThat(md.template())
153157
.hasHeaders(
154158
entry("Content-Type", asList("application/xml")),
155-
entry("Content-Length", asList(String.valueOf(md.template().body().length))));
159+
entry(
160+
"Content-Length",
161+
asList(String.valueOf(md.template().requestBody().asBytes().length))));
156162
}
157163

158164
@Test

core/src/test/java/feign/template/QueryTemplateTest.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,35 @@ public void collectionFormat() {
7070
String expanded = template.expand(Collections.emptyMap());
7171
assertThat(expanded).isEqualToIgnoringCase("name=James,Jason");
7272
}
73+
74+
@Test
75+
public void expandName() {
76+
QueryTemplate template =
77+
QueryTemplate.create("{name}", Arrays.asList("James", "Jason"), Util.UTF_8);
78+
String expanded = template.expand(Collections.singletonMap("name", "firsts"));
79+
assertThat(expanded).isEqualToIgnoringCase("firsts=James&firsts=Jason");
80+
}
81+
82+
@Test
83+
public void expandPureParameter() {
84+
QueryTemplate template = QueryTemplate.create("{name}", Collections.emptyList(), Util.UTF_8);
85+
String expanded = template.expand(Collections.singletonMap("name", "firsts"));
86+
assertThat(expanded).isEqualToIgnoringCase("firsts");
87+
}
88+
89+
@Test
90+
public void expandPureParameterWithSlash() {
91+
QueryTemplate template =
92+
QueryTemplate.create("/path/{name}", Collections.emptyList(), Util.UTF_8);
93+
String expanded = template.expand(Collections.singletonMap("name", "firsts"));
94+
assertThat(expanded).isEqualToIgnoringCase("/path/firsts");
95+
}
96+
97+
@Test
98+
public void expandNameUnresolved() {
99+
QueryTemplate template =
100+
QueryTemplate.create("{parameter}", Arrays.asList("James", "Jason"), Util.UTF_8);
101+
String expanded = template.expand(Collections.singletonMap("name", "firsts"));
102+
assertThat(expanded).isEqualToIgnoringCase("%7Bparameter%7D=James&%7Bparameter%7D=Jason");
103+
}
73104
}

0 commit comments

Comments
 (0)