Skip to content

Commit

Permalink
[OpenFeignGH-1319] Add Support for Path Style Parameter Expansion
Browse files Browse the repository at this point in the history
Fixes OpenFeign#1319

This change adds limited Path Style support to Feign URI template-style
templates.  Variable expressions that start with a semi-colon `;`
are now expanded in accordance to [RFC 6570 Section 3.2.7](https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.7)
with the following modifications:

* Maps and Lists are expanded by default.
* Only Single variable templates are supported.

Examples:
```
{;who}             ;who=fred
{;half}            ;half=50%25
{;empty}           ;empty
{;list}            ;list=red;list=green;list=blue
{;keys}            ;semi=%3B;dot=.;comma=%2C
```
  • Loading branch information
kdavisk6 committed Mar 24, 2022
1 parent c2690d6 commit 21112f6
Show file tree
Hide file tree
Showing 2 changed files with 127 additions and 43 deletions.
123 changes: 80 additions & 43 deletions core/src/main/java/feign/template/Expressions.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,32 +13,17 @@
*/
package feign.template;

import feign.Util;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import feign.Util;

public final class Expressions {
private static Map<Pattern, Class<? extends Expression>> expressions;

static {
expressions = new LinkedHashMap<>();

/*
* basic pattern for variable names. this is compliant with RFC 6570 Simple Expressions ONLY
* with the following additional values allowed without required pct-encoding:
*
* - brackets - dashes
*
* see https://tools.ietf.org/html/rfc6570#section-2.3 for more information.
*/

expressions.put(Pattern.compile("^([+#./;?&]?)(.*)$"),
SimpleExpression.class);
}

private static final String PATH_STYLE_MODIFIER = ";";
private static final Pattern EXPRESSION_PATTERN = Pattern.compile("^([+#./;?&]?)(.*)$");

public static Expression create(final String value) {

Expand All @@ -48,25 +33,15 @@ public static Expression create(final String value) {
throw new IllegalArgumentException("an expression is required.");
}

Optional<Entry<Pattern, Class<? extends Expression>>> matchedExpressionEntry =
expressions.entrySet()
.stream()
.filter(entry -> entry.getKey().matcher(expression).matches())
.findFirst();

if (!matchedExpressionEntry.isPresent()) {
/* not a valid expression */
return null;
}

Entry<Pattern, Class<? extends Expression>> matchedExpression = matchedExpressionEntry.get();
Pattern expressionPattern = matchedExpression.getKey();

/* create a new regular expression matcher for the expression */
String variableName = null;
String variablePattern = null;
Matcher matcher = expressionPattern.matcher(expression);
String modifier = null;
Matcher matcher = EXPRESSION_PATTERN.matcher(expression);
if (matcher.matches()) {
/* grab the modifier */
modifier = matcher.group(1).trim();

/* we have a valid variable expression, extract the name from the first group */
variableName = matcher.group(2).trim();
if (variableName.contains(":")) {
Expand All @@ -83,6 +58,12 @@ public static Expression create(final String value) {
}
}

/* check for a modifier */
if (PATH_STYLE_MODIFIER.equalsIgnoreCase(modifier)) {
return new PathStyleExpression(variableName, variablePattern);
}

/* default to simple */
return new SimpleExpression(variableName, variablePattern);
}

Expand All @@ -102,20 +83,37 @@ private static String stripBraces(String expression) {
*/
static class SimpleExpression extends Expression {

SimpleExpression(String expression, String pattern) {
super(expression, pattern);
private static final String DEFAULT_SEPARATOR = ",";
protected String separator = DEFAULT_SEPARATOR;
private boolean nameRequired = false;

SimpleExpression(String name, String pattern) {
super(name, pattern);
}

SimpleExpression(String name, String pattern, String separator, boolean nameRequired) {
this(name, pattern);
this.separator = separator;
this.nameRequired = nameRequired;
}

String encode(Object value) {
protected String encode(Object value) {
return UriUtils.encode(value.toString(), Util.UTF_8);
}

@SuppressWarnings("unchecked")
@Override
String expand(Object variable, boolean encode) {
protected String expand(Object variable, boolean encode) {
StringBuilder expanded = new StringBuilder();
if (Iterable.class.isAssignableFrom(variable.getClass())) {
expanded.append(this.expandIterable((Iterable<?>) variable));
} else if (Map.class.isAssignableFrom(variable.getClass())) {
expanded.append(this.expandMap((Map<String, ?>) variable));
} else {
if (this.nameRequired) {
expanded.append(this.encode(this.getName()))
.append("=");
}
expanded.append((encode) ? encode(variable) : variable);
}

Expand All @@ -128,8 +126,7 @@ String expand(Object variable, boolean encode) {
return result;
}


private String expandIterable(Iterable<?> values) {
protected String expandIterable(Iterable<?> values) {
StringBuilder result = new StringBuilder();
for (Object value : values) {
if (value == null) {
Expand All @@ -141,19 +138,59 @@ private String expandIterable(Iterable<?> values) {
String expanded = this.encode(value);
if (expanded.isEmpty()) {
/* always append the separator */
result.append(",");
result.append(this.separator);
} else {
if (result.length() != 0) {
if (!result.toString().equalsIgnoreCase(",")) {
result.append(",");
if (!result.toString().equalsIgnoreCase(this.separator)) {
result.append(this.separator);
}
}
if (this.nameRequired) {
result.append(this.encode(this.getName()))
.append("=");
}
result.append(expanded);
}
}

/* return the expanded value */
return result.toString();
}

protected String expandMap(Map<String, ?> values) {
StringBuilder result = new StringBuilder();

for (Entry<String, ?> entry : values.entrySet()) {
StringBuilder expanded = new StringBuilder();
String name = this.encode(entry.getKey());
String value = this.encode(entry.getValue().toString());

expanded.append(name)
.append("=");
if (!value.isEmpty()) {
expanded.append(value);
}

if (result.length() != 0) {
result.append(this.separator);
}

result.append(expanded);
}
return result.toString();
}
}

static class PathStyleExpression extends SimpleExpression {

PathStyleExpression(String name, String pattern) {
super(name, pattern, ";", true);
}

@Override
protected String expand(Object variable, boolean encode) {
return this.separator + super.expand(variable, encode);
}

}
}
47 changes: 47 additions & 0 deletions core/src/test/java/feign/template/UriTemplateTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import static org.assertj.core.api.Assertions.fail;
import feign.Util;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
Expand Down Expand Up @@ -299,4 +300,50 @@ public void encodeReserved() {
String expanded = uriTemplate.expand(Collections.singletonMap("url", "https://www.google.com"));
assertThat(expanded).isEqualToIgnoringCase("/get?url=https%3A%2F%2Fwww.google.com");
}

@Test
public void pathStyleExpansionSupported() {
String template = "{;who}";
UriTemplate uriTemplate= UriTemplate.create(template, Util.UTF_8);
String expanded = uriTemplate.expand(Collections.singletonMap("who", "fred"));
assertThat(expanded).isEqualToIgnoringCase(";who=fred");
}

@Test
public void pathStyleExpansionEncodesReservedCharacters() {
String template = "{;half}";
UriTemplate uriTemplate= UriTemplate.create(template, Util.UTF_8);
String expanded = uriTemplate.expand(Collections.singletonMap("half", "50%"));
assertThat(expanded).isEqualToIgnoringCase(";half=50%25");
}

@Test
public void pathStyleExpansionSupportedWithLists() {
String template = "{;list}";
UriTemplate uriTemplate= UriTemplate.create(template, Util.UTF_8);

List<String> values = new ArrayList<>();
values.add("red");
values.add("green");
values.add("blue");

String expanded = uriTemplate.expand(Collections.singletonMap("list", values));
assertThat(expanded).isEqualToIgnoringCase(";list=red;list=green;list=blue");

}

@Test
public void pathStyleExpansionSupportedWithMap() {
String template = "/server/matrixParams{;parameters}";
Map<String, Object> parameters = new LinkedHashMap<>();
parameters.put("account", "a");
parameters.put("name", "n");

Map<String, Object> values = new LinkedHashMap<>();
values.put("parameters", parameters);

UriTemplate uriTemplate = UriTemplate.create(template, Util.UTF_8);
String expanded = uriTemplate.expand(values);
assertThat(expanded).isEqualToIgnoringCase("/server/matrixParams;account=a;name=n");
}
}

0 comments on commit 21112f6

Please sign in to comment.