From 971f0469133e845c9befb0260db6a27b88e31b7c Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 5 Nov 2015 09:36:02 -0500 Subject: [PATCH] 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 --- .../springframework/web/util/UriTemplate.java | 133 ++++++++++-------- .../web/util/UriTemplateTests.java | 9 ++ 2 files changed, 84 insertions(+), 58 deletions(-) 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}");