Skip to content

Commit ffeb775

Browse files
JKomoroskikdavisk6
andauthored
Implement a Regex based Check on Expressions (#1776)
Co-authored-by: Kevin Davis <kdavisk6@gmail.com>
1 parent e3db282 commit ffeb775

File tree

3 files changed

+92
-11
lines changed

3 files changed

+92
-11
lines changed

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

Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,50 @@
1515

1616
import feign.Param.Expander;
1717
import feign.Util;
18-
import java.util.LinkedHashMap;
1918
import java.util.Map;
2019
import java.util.Map.Entry;
2120
import java.util.regex.Matcher;
2221
import java.util.regex.Pattern;
2322

2423
public final class Expressions {
2524

26-
private static final String PATH_STYLE_MODIFIER = ";";
27-
private static final Pattern EXPRESSION_PATTERN = Pattern.compile("^([+#./;?&]?)(.*)$");
25+
private static final String PATH_STYLE_OPERATOR = ";";
26+
/**
27+
* Literals may be present and preceded the expression.
28+
*
29+
* The expression part must start with a '{' and end with a '}'. The contents of the expression
30+
* may start with an RFC Operator or the operators reserved by the rfc: Level 2 Operators: '+' and
31+
* '#' Level 3 Operators: '.' and '/' and ';' and '?' and '&' Reserved Operators: '=' and ',' and
32+
* '!' and '@' and '|'
33+
*
34+
* The RFC specifies that '{' or '}' or '(' or ')' or'$' is are illegal characters. Feign does not
35+
* honor this portion of the RFC Expressions allow '$' characters for Collection expansions, and
36+
* all other characters are legal as a regular expression may be passed as a Value Modifier in
37+
* Feign
38+
*
39+
* This is not a complete implementation of the rfc
40+
*
41+
* <a href="https://www.rfc-editor.org/rfc/rfc6570#section-2.2>RFC 6570 Expressions</a>
42+
*/
43+
private static final Pattern EXPRESSION_PATTERN =
44+
Pattern.compile("^(\\{([+#./;?&=,!@|]?)(.+)})$");
45+
46+
// Partially From:
47+
// https://stackoverflow.com/questions/29494608/regex-for-uri-templates-rfc-6570-wanted -- I
48+
// suspect much of the codebase could be refactored around the example regex there
49+
/**
50+
* A pattern for matching possible variable names.
51+
*
52+
* This pattern accepts characters allowed in RFC 6570 Section 2.3 It also allows the characters
53+
* feign has allowed in the past "[]-$"
54+
*
55+
* The RFC specifies that a variable name followed by a ':' should be a max-length specification.
56+
* Feign deviates from the rfc in that the ':' value modifier is used to mark a regular
57+
* expression.
58+
*
59+
*/
60+
private static final Pattern VARIABLE_LIST_PATTERN = Pattern.compile(
61+
"(([\\w-\\[\\]$]|%[0-9A-Fa-f]{2})(\\.?([\\w-\\[\\]$]|%[0-9A-Fa-f]{2}))*(:.*|\\*)?)(,(([\\w-\\[\\]$]|%[0-9A-Fa-f]{2})(\\.?([\\w-\\[\\]$]|%[0-9A-Fa-f]{2}))*(:.*|\\*)?))*");
2862

2963
public static Expression create(final String value) {
3064

@@ -37,14 +71,14 @@ public static Expression create(final String value) {
3771
/* create a new regular expression matcher for the expression */
3872
String variableName = null;
3973
String variablePattern = null;
40-
String modifier = null;
41-
Matcher matcher = EXPRESSION_PATTERN.matcher(expression);
74+
String operator = null;
75+
Matcher matcher = EXPRESSION_PATTERN.matcher(value);
4276
if (matcher.matches()) {
43-
/* grab the modifier */
44-
modifier = matcher.group(1).trim();
77+
/* grab the operator */
78+
operator = matcher.group(2).trim();
4579

4680
/* we have a valid variable expression, extract the name from the first group */
47-
variableName = matcher.group(2).trim();
81+
variableName = matcher.group(3).trim();
4882
if (variableName.contains(":")) {
4983
/* split on the colon */
5084
String[] parts = variableName.split(":");
@@ -59,13 +93,15 @@ public static Expression create(final String value) {
5993
}
6094
}
6195

62-
/* check for a modifier */
63-
if (PATH_STYLE_MODIFIER.equalsIgnoreCase(modifier)) {
96+
/* check for an operator */
97+
if (PATH_STYLE_OPERATOR.equalsIgnoreCase(operator)) {
6498
return new PathStyleExpression(variableName, variablePattern);
6599
}
66100

67101
/* default to simple */
68-
return new SimpleExpression(variableName, variablePattern);
102+
return SimpleExpression.isSimpleExpression(value)
103+
? new SimpleExpression(variableName, variablePattern)
104+
: null; // Return null if it can't be validated as a Simple Expression -- Probably a Literal
69105
}
70106

71107
private static String stripBraces(String expression) {
@@ -180,6 +216,13 @@ protected String expandMap(Map<String, ?> values) {
180216
}
181217
return result.toString();
182218
}
219+
220+
protected static boolean isSimpleExpression(String expressionCandidate) {
221+
final Matcher matcher = EXPRESSION_PATTERN.matcher(expressionCandidate);
222+
return matcher.matches()
223+
&& matcher.group(2).isEmpty() // Simple Expressions do not support any special operators
224+
&& VARIABLE_LIST_PATTERN.matcher(matcher.group(3)).matches();
225+
}
183226
}
184227

185228
public static class PathStyleExpression extends SimpleExpression implements Expander {

core/src/test/java/feign/RequestTemplateTest.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,31 @@ public void resolveTemplateWithHeaderWithEscapedCurlyBrace() {
206206
.hasHeaders(entry("Encoded", Collections.singletonList("{{{{dont_expand_me}}")));
207207
}
208208

209+
@Test
210+
public void resolveTemplateWithHeaderWithJson() {
211+
String json = "{ \"string\": \"val\", \"string2\": \"this should not be truncated\"}";
212+
RequestTemplate template = new RequestTemplate().method(HttpMethod.GET)
213+
.header("A-Header", "{aHeader}");
214+
215+
template = template.resolve(mapOf("aHeader", json));
216+
217+
assertThat(template)
218+
.hasHeaders(entry("A-Header", Collections.singletonList(json)));
219+
}
220+
221+
@Test
222+
public void resolveTemplateWithHeaderWithNestedJson() {
223+
String json =
224+
"{ \"string\": \"val\", \"string2\": \"this should not be truncated\", \"property\": {\"nested\": true}}";
225+
RequestTemplate template = new RequestTemplate().method(HttpMethod.GET)
226+
.header("A-Header", "{aHeader}");
227+
228+
template = template.resolve(mapOf("aHeader", json));
229+
230+
assertThat(template)
231+
.hasHeaders(entry("A-Header", Collections.singletonList(json)));
232+
}
233+
209234
/**
210235
* This ensures we don't mess up vnd types
211236
*/

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,4 +113,17 @@ public void it_should_support_http_date() {
113113
headerTemplate.expand(
114114
Collections.singletonMap("expires", "Wed, 4 Jul 2001 12:08:56 -0700")));
115115
}
116+
117+
@Test
118+
public void it_should_support_json_literal_values() {
119+
HeaderTemplate headerTemplate =
120+
HeaderTemplate.create("CustomHeader", Collections.singletonList("{jsonParam}"));
121+
122+
assertEquals("{\"string\": \"val\", \"string2\": \"this should not be truncated\"}",
123+
headerTemplate.expand(
124+
Collections.singletonMap(
125+
"jsonParam",
126+
"{\"string\": \"val\", \"string2\": \"this should not be truncated\"}")));
127+
128+
}
116129
}

0 commit comments

Comments
 (0)