diff --git a/spring-web/src/main/java/org/springframework/web/util/UriTemplate.java b/spring-web/src/main/java/org/springframework/web/util/UriTemplate.java
index 17d98f7b8e..95e8c2d6b7 100644
--- a/spring-web/src/main/java/org/springframework/web/util/UriTemplate.java
+++ b/spring-web/src/main/java/org/springframework/web/util/UriTemplate.java
@@ -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;
*
* @author Arjen Poutsma
* @author Juergen Hoeller
+ * @author Rossen Stoyanchev
* @since 3.0
* @see URI Templates
*/
@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 variableNames;
@@ -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 {
/**
- * 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 variableNames = new LinkedList();
-
- 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 variableNames;
+
+ private final Pattern pattern;
+
+
+ private TemplateInfo(List 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 getVariableNames() {
+ return this.variableNames;
+ }
+
+ public Pattern getMatchPattern() {
+ return this.pattern;
}
- private List getVariableNames() {
- return Collections.unmodifiableList(this.variableNames);
+ private static TemplateInfo parse(String uriTemplate) {
+ int level = 0;
+ List variableNames = new ArrayList();
+ 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()) : "";
}
}
diff --git a/spring-web/src/test/java/org/springframework/web/util/UriTemplateTests.java b/spring-web/src/test/java/org/springframework/web/util/UriTemplateTests.java
index 47e22650e9..1e0d6c2316 100644
--- a/spring-web/src/test/java/org/springframework/web/util/UriTemplateTests.java
+++ b/spring-web/src/test/java/org/springframework/web/util/UriTemplateTests.java
@@ -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 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}");