Browse Source

Replace NAMES_PATTERN in UriTemplate

The URI template is now manually parsed vs using a regex to extract
URI variable names and to create a pattern for matching to actual URLs.
This provides more control to deal with nested curly braces.

Issue: SPR-13627
pull/1619/head
Rossen Stoyanchev 9 years ago
parent
commit
971f046913
  1. 133
      spring-web/src/main/java/org/springframework/web/util/UriTemplate.java
  2. 9
      spring-web/src/test/java/org/springframework/web/util/UriTemplateTests.java

133
spring-web/src/main/java/org/springframework/web/util/UriTemplate.java

@ -18,9 +18,9 @@ package org.springframework.web.util; @@ -18,9 +18,9 @@ package org.springframework.web.util;
import java.io.Serializable;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
@ -37,19 +37,13 @@ import org.springframework.util.Assert; @@ -37,19 +37,13 @@ import org.springframework.util.Assert;
*
* @author Arjen Poutsma
* @author Juergen Hoeller
* @author Rossen Stoyanchev
* @since 3.0
* @see <a href="http://bitworking.org/projects/URI-Templates/">URI Templates</a>
*/
@SuppressWarnings("serial")
public class UriTemplate implements Serializable {
/** Captures URI template variable names. */
private static final Pattern NAMES_PATTERN = Pattern.compile("\\{([^/]+?)\\}");
/** Replaces template variables in the URI template. */
private static final String DEFAULT_VARIABLE_PATTERN = "(.*)";
private final UriComponents uriComponents;
private final List<String> variableNames;
@ -64,11 +58,13 @@ public class UriTemplate implements Serializable { @@ -64,11 +58,13 @@ public class UriTemplate implements Serializable {
* @param uriTemplate the URI template string
*/
public UriTemplate(String uriTemplate) {
Parser parser = new Parser(uriTemplate);
Assert.hasText(uriTemplate, "'uriTemplate' must not be null");
this.uriTemplate = uriTemplate;
this.variableNames = parser.getVariableNames();
this.matchPattern = parser.getMatchPattern();
this.uriComponents = UriComponentsBuilder.fromUriString(uriTemplate).build();
TemplateInfo info = TemplateInfo.parse(uriTemplate);
this.variableNames = Collections.unmodifiableList(info.getVariableNames());
this.matchPattern = info.getMatchPattern();
}
@ -169,60 +165,81 @@ public class UriTemplate implements Serializable { @@ -169,60 +165,81 @@ public class UriTemplate implements Serializable {
/**
* Static inner class to parse URI template strings into a matching regular expression.
* Helper to extract variable names and regex for matching to actual URLs.
*/
private static class Parser {
private final List<String> variableNames = new LinkedList<String>();
private final StringBuilder patternBuilder = new StringBuilder();
private Parser(String uriTemplate) {
Assert.hasText(uriTemplate, "'uriTemplate' must not be null");
Matcher matcher = NAMES_PATTERN.matcher(uriTemplate);
int end = 0;
while (matcher.find()) {
this.patternBuilder.append(quote(uriTemplate, end, matcher.start()));
String match = matcher.group(1);
int colonIdx = match.indexOf(':');
if (colonIdx == -1) {
this.patternBuilder.append(DEFAULT_VARIABLE_PATTERN);
this.variableNames.add(match);
}
else {
if (colonIdx + 1 == match.length()) {
throw new IllegalArgumentException(
"No custom regular expression specified after ':' in \"" + match + "\"");
}
String variablePattern = match.substring(colonIdx + 1, match.length());
this.patternBuilder.append('(');
this.patternBuilder.append(variablePattern);
this.patternBuilder.append(')');
String variableName = match.substring(0, colonIdx);
this.variableNames.add(variableName);
}
end = matcher.end();
}
this.patternBuilder.append(quote(uriTemplate, end, uriTemplate.length()));
int lastIdx = this.patternBuilder.length() - 1;
if (lastIdx >= 0 && this.patternBuilder.charAt(lastIdx) == '/') {
this.patternBuilder.deleteCharAt(lastIdx);
}
private static class TemplateInfo {
private final List<String> variableNames;
private final Pattern pattern;
private TemplateInfo(List<String> vars, Pattern pattern) {
this.variableNames = vars;
this.pattern = pattern;
}
private String quote(String fullPath, int start, int end) {
if (start == end) {
return "";
}
return Pattern.quote(fullPath.substring(start, end));
public List<String> getVariableNames() {
return this.variableNames;
}
public Pattern getMatchPattern() {
return this.pattern;
}
private List<String> getVariableNames() {
return Collections.unmodifiableList(this.variableNames);
private static TemplateInfo parse(String uriTemplate) {
int level = 0;
List<String> variableNames = new ArrayList<String>();
StringBuilder pattern = new StringBuilder();
StringBuilder builder = new StringBuilder();
for (int i = 0 ; i < uriTemplate.length(); i++) {
char c = uriTemplate.charAt(i);
if (c == '{') {
level++;
if (level == 1) {
pattern.append(quote(builder));
builder = new StringBuilder();
continue;
}
}
else if (c == '}') {
level--;
if (level == 0) {
String variable = builder.toString();
int idx = variable.indexOf(':');
if (idx == -1) {
pattern.append("(.*)");
variableNames.add(variable);
}
else {
if (idx + 1 == variable.length()) {
throw new IllegalArgumentException(
"No custom regular expression specified after ':' " +
"in \"" + variable + "\"");
}
String regex = variable.substring(idx + 1, variable.length());
pattern.append('(');
pattern.append(regex);
pattern.append(')');
variableNames.add(variable.substring(0, idx));
}
builder = new StringBuilder();
continue;
}
}
if (i + 1 == uriTemplate.length()) {
if (c != '/') {
builder.append(c);
}
pattern.append(quote(builder));
}
builder.append(c);
}
return new TemplateInfo(variableNames, Pattern.compile(pattern.toString()));
}
private Pattern getMatchPattern() {
return Pattern.compile(this.patternBuilder.toString());
private static String quote(StringBuilder builder) {
return builder.length() != 0 ? Pattern.quote(builder.toString()) : "";
}
}

9
spring-web/src/test/java/org/springframework/web/util/UriTemplateTests.java

@ -143,6 +143,15 @@ public class UriTemplateTests { @@ -143,6 +143,15 @@ public class UriTemplateTests {
assertEquals("Invalid match", expected, result);
}
// SPR-13627
@Test
public void matchCustomRegexWithNestedCurlyBraces() throws Exception {
UriTemplate template = new UriTemplate("/site.{domain:co.[a-z]{2}}");
Map<String, String> result = template.match("/site.co.eu");
assertEquals("Invalid match", Collections.singletonMap("domain", "co.eu"), result);
}
@Test
public void matchDuplicate() throws Exception {
UriTemplate template = new UriTemplate("/order/{c}/{c}/{c}");

Loading…
Cancel
Save