Skip to content

Commit c178888

Browse files
committed
SPR-5537: ReSTful URLs with content type extension do not work properly
1 parent 0b463c0 commit c178888

File tree

3 files changed

+73
-196
lines changed

3 files changed

+73
-196
lines changed

org.springframework.core/src/main/java/org/springframework/util/AntPatchStringMatcher.java

Lines changed: 47 additions & 192 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,15 @@
1616

1717
package org.springframework.util;
1818

19+
import java.util.LinkedList;
20+
import java.util.List;
1921
import java.util.Map;
22+
import java.util.regex.Matcher;
23+
import java.util.regex.Pattern;
2024

2125
/**
2226
* Package-protected helper class for {@link AntPathMatcher}.
23-
* Tests whether or not a string matches against a pattern.
27+
* Tests whether or not a string matches against a pattern using a regular expression.
2428
*
2529
* <p>The pattern may contain special characters: '*' means zero or more characters;
2630
* '?' means one and only one character; '{' and '}' indicate a URI template pattern.
@@ -30,189 +34,66 @@
3034
*/
3135
class AntPatchStringMatcher {
3236

33-
private final char[] patArr;
37+
private static final Pattern GLOB_PATTERN = Pattern.compile("\\?|\\*|\\{([^/]+?)\\}");
3438

35-
private final char[] strArr;
39+
private final Pattern pattern;
3640

37-
private int patIdxStart = 0;
41+
private String str;
3842

39-
private int patIdxEnd;
40-
41-
private int strIdxStart = 0;
42-
43-
private int strIdxEnd;
44-
45-
private char ch;
43+
private final List<String> variableNames = new LinkedList<String>();
4644

4745
private final Map<String, String> uriTemplateVariables;
4846

49-
50-
/**
51-
* Construct a new instance of the <code>AntPatchStringMatcher</code>.
52-
*/
53-
public AntPatchStringMatcher(String pattern, String str, Map<String, String> uriTemplateVariables) {
54-
this.patArr = pattern.toCharArray();
55-
this.strArr = str.toCharArray();
56-
this.patIdxEnd = this.patArr.length - 1;
57-
this.strIdxEnd = this.strArr.length - 1;
47+
/** Construct a new instance of the <code>AntPatchStringMatcher</code>. */
48+
AntPatchStringMatcher(String pattern, String str, Map<String, String> uriTemplateVariables) {
49+
this.str = str;
5850
this.uriTemplateVariables = uriTemplateVariables;
51+
this.pattern = createPattern(pattern);
5952
}
6053

61-
62-
/**
63-
* Main entry point.
64-
* @return <code>true</code> if the string matches against the pattern, or <code>false</code> otherwise.
65-
*/
66-
public boolean matchStrings() {
67-
if (shortcutPossible()) {
68-
return doShortcut();
69-
}
70-
if (patternContainsOnlyStar()) {
71-
return true;
72-
}
73-
if (patternContainsOneTemplateVariable()) {
74-
addTemplateVariable(0, patIdxEnd, 0, strIdxEnd);
75-
return true;
76-
}
77-
if (!matchBeforeFirstStarOrCurly()) {
78-
return false;
79-
}
80-
if (allCharsUsed()) {
81-
return onlyStarsLeft();
82-
}
83-
if (!matchAfterLastStarOrCurly()) {
84-
return false;
85-
}
86-
if (allCharsUsed()) {
87-
return onlyStarsLeft();
88-
}
89-
// process pattern between stars. padIdxStart and patIdxEnd point
90-
// always to a '*'.
91-
while (patIdxStart != patIdxEnd && strIdxStart <= strIdxEnd) {
92-
int patIdxTmp;
93-
if (patArr[patIdxStart] == '{') {
94-
patIdxTmp = findClosingCurly();
95-
addTemplateVariable(patIdxStart, patIdxTmp, strIdxStart, strIdxEnd);
96-
patIdxStart = patIdxTmp + 1;
97-
strIdxStart = strIdxEnd + 1;
98-
continue;
99-
}
100-
patIdxTmp = findNextStarOrCurly();
101-
if (consecutiveStars(patIdxTmp)) {
102-
continue;
103-
}
104-
// Find the pattern between padIdxStart & padIdxTmp in str between
105-
// strIdxStart & strIdxEnd
106-
int patLength = (patIdxTmp - patIdxStart - 1);
107-
int strLength = (strIdxEnd - strIdxStart + 1);
108-
int foundIdx = -1;
109-
strLoop:
110-
for (int i = 0; i <= strLength - patLength; i++) {
111-
for (int j = 0; j < patLength; j++) {
112-
ch = patArr[patIdxStart + j + 1];
113-
if (ch != '?') {
114-
if (ch != strArr[strIdxStart + i + j]) {
115-
continue strLoop;
116-
}
117-
}
118-
}
119-
120-
foundIdx = strIdxStart + i;
121-
break;
122-
}
123-
124-
if (foundIdx == -1) {
125-
return false;
126-
}
127-
128-
patIdxStart = patIdxTmp;
129-
strIdxStart = foundIdx + patLength;
130-
}
131-
132-
return onlyStarsLeft();
133-
}
134-
135-
private void addTemplateVariable(int curlyIdxStart, int curlyIdxEnd, int valIdxStart, int valIdxEnd) {
136-
if (uriTemplateVariables != null) {
137-
String varName = new String(patArr, curlyIdxStart + 1, curlyIdxEnd - curlyIdxStart - 1);
138-
String varValue = new String(strArr, valIdxStart, valIdxEnd - valIdxStart + 1);
139-
uriTemplateVariables.put(varName, varValue);
140-
}
141-
}
142-
143-
private boolean consecutiveStars(int patIdxTmp) {
144-
if (patIdxTmp == patIdxStart + 1 && patArr[patIdxStart] == '*' && patArr[patIdxTmp] == '*') {
145-
// Two stars next to each other, skip the first one.
146-
patIdxStart++;
147-
return true;
148-
}
149-
return false;
150-
}
151-
152-
private int findNextStarOrCurly() {
153-
for (int i = patIdxStart + 1; i <= patIdxEnd; i++) {
154-
if (patArr[i] == '*' || patArr[i] == '{') {
155-
return i;
54+
private Pattern createPattern(String pattern) {
55+
StringBuilder patternBuilder = new StringBuilder();
56+
Matcher m = GLOB_PATTERN.matcher(pattern);
57+
int end = 0;
58+
while (m.find()) {
59+
patternBuilder.append(quote(pattern, end, m.start()));
60+
String match = m.group();
61+
if ("?".equals(match)) {
62+
patternBuilder.append('.');
15663
}
157-
}
158-
return -1;
159-
}
160-
161-
private int findClosingCurly() {
162-
for (int i = patIdxStart + 1; i <= patIdxEnd; i++) {
163-
if (patArr[i] == '}') {
164-
return i;
64+
else if ("*".equals(match)) {
65+
patternBuilder.append(".*");
16566
}
166-
}
167-
return -1;
168-
}
169-
170-
private boolean onlyStarsLeft() {
171-
for (int i = patIdxStart; i <= patIdxEnd; i++) {
172-
if (patArr[i] != '*') {
173-
return false;
67+
else if (match.startsWith("{") && match.endsWith("}")) {
68+
patternBuilder.append("(.*)");
69+
variableNames.add(m.group(1));
17470
}
71+
end = m.end();
17572
}
176-
return true;
177-
}
178-
179-
private boolean allCharsUsed() {
180-
return strIdxStart > strIdxEnd;
73+
patternBuilder.append(quote(pattern, end, pattern.length()));
74+
return Pattern.compile(patternBuilder.toString());
18175
}
18276

183-
private boolean shortcutPossible() {
184-
for (char ch : patArr) {
185-
if (ch == '*' || ch == '{' || ch == '}') {
186-
return false;
187-
}
77+
private String quote(String s, int start, int end) {
78+
if (start == end) {
79+
return "";
18880
}
189-
return true;
81+
return Pattern.quote(s.substring(start, end));
19082
}
19183

192-
private boolean doShortcut() {
193-
if (patIdxEnd != strIdxEnd) {
194-
return false; // Pattern and string do not have the same size
195-
}
196-
for (int i = 0; i <= patIdxEnd; i++) {
197-
ch = patArr[i];
198-
if (ch != '?') {
199-
if (ch != strArr[i]) {
200-
return false;// Character mismatch
201-
}
202-
}
203-
}
204-
return true; // String matches against pattern
205-
}
206-
207-
private boolean patternContainsOnlyStar() {
208-
return (patIdxEnd == 0 && patArr[0] == '*');
209-
}
210-
211-
private boolean patternContainsOneTemplateVariable() {
212-
if ((patIdxEnd >= 2 && patArr[0] == '{' && patArr[patIdxEnd] == '}')) {
213-
for (int i = 1; i < patIdxEnd; i++) {
214-
if (patArr[i] == '}') {
215-
return false;
84+
/**
85+
* Main entry point.
86+
*
87+
* @return <code>true</code> if the string matches against the pattern, or <code>false</code> otherwise.
88+
*/
89+
public boolean matchStrings() {
90+
Matcher matcher = pattern.matcher(str);
91+
if (matcher.matches()) {
92+
if (uriTemplateVariables != null) {
93+
for (int i = 1; i <= matcher.groupCount(); i++) {
94+
String name = this.variableNames.get(i - 1);
95+
String value = matcher.group(i);
96+
uriTemplateVariables.put(name, value);
21697
}
21798
}
21899
return true;
@@ -222,30 +103,4 @@ private boolean patternContainsOneTemplateVariable() {
222103
}
223104
}
224105

225-
private boolean matchBeforeFirstStarOrCurly() {
226-
while ((ch = patArr[patIdxStart]) != '*' && ch != '{' && strIdxStart <= strIdxEnd) {
227-
if (ch != '?') {
228-
if (ch != strArr[strIdxStart]) {
229-
return false;
230-
}
231-
}
232-
patIdxStart++;
233-
strIdxStart++;
234-
}
235-
return true;
236-
}
237-
238-
private boolean matchAfterLastStarOrCurly() {
239-
while ((ch = patArr[patIdxEnd]) != '*' && ch != '}' && strIdxStart <= strIdxEnd) {
240-
if (ch != '?') {
241-
if (ch != strArr[strIdxEnd]) {
242-
return false;
243-
}
244-
}
245-
patIdxEnd--;
246-
strIdxEnd--;
247-
}
248-
return true;
249-
}
250-
251106
}

org.springframework.core/src/test/java/org/springframework/util/AntPathMatcherTests.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public void createMatcher() {
4343
}
4444

4545
@Test
46-
public void standard() {
46+
public void match() {
4747
// test exact matching
4848
assertTrue(pathMatcher.match("test", "test"));
4949
assertTrue(pathMatcher.match("/test", "/test"));
@@ -123,6 +123,8 @@ public void standard() {
123123
assertFalse(pathMatcher.match("/x/x/**/bla", "/x/x/x/"));
124124

125125
assertTrue(pathMatcher.match("", ""));
126+
127+
assertTrue(pathMatcher.match("/{bla}.*", "/testing.html"));
126128
}
127129

128130
@Test
@@ -319,8 +321,17 @@ public void extractUriTemplateVariables() throws Exception {
319321
result = pathMatcher.extractUriTemplateVariables("/{page}.html", "/42.html");
320322
assertEquals(Collections.singletonMap("page", "42"), result);
321323

324+
result = pathMatcher.extractUriTemplateVariables("/{page}.*", "/42.html");
325+
assertEquals(Collections.singletonMap("page", "42"), result);
326+
322327
result = pathMatcher.extractUriTemplateVariables("/A-{B}-C", "/A-b-C");
323328
assertEquals(Collections.singletonMap("B", "b"), result);
329+
330+
result = pathMatcher.extractUriTemplateVariables("/{name}.{extension}", "/test.html");
331+
expected = new LinkedHashMap<String, String>();
332+
expected.put("name", "test");
333+
expected.put("extension", "html");
334+
assertEquals(expected, result);
324335
}
325336

326337
@Test

org.springframework.web.servlet/src/test/java/org/springframework/web/servlet/mvc/annotation/UriTemplateServletAnnotationControllerTests.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import java.util.Date;
77
import javax.servlet.ServletException;
88

9-
import static org.junit.Assert.assertEquals;
9+
import static org.junit.Assert.*;
1010
import org.junit.Test;
1111

1212
import org.springframework.beans.BeansException;
@@ -81,6 +81,17 @@ public void relative() throws Exception {
8181
assertEquals("test-42-21", response.getContentAsString());
8282
}
8383

84+
@Test
85+
public void extension() throws Exception {
86+
initServlet(SimpleUriTemplateController.class);
87+
88+
MockHttpServletRequest request = new MockHttpServletRequest("GET", "/42.xml");
89+
MockHttpServletResponse response = new MockHttpServletResponse();
90+
servlet.service(request, response);
91+
assertEquals("test-42", response.getContentAsString());
92+
93+
}
94+
8495
private void initServlet(final Class<?> controllerclass) throws ServletException {
8596
servlet = new DispatcherServlet() {
8697
@Override
@@ -103,8 +114,8 @@ protected WebApplicationContext createWebApplicationContext(WebApplicationContex
103114
public static class SimpleUriTemplateController {
104115

105116
@RequestMapping("/{root}")
106-
public void handle(@PathVariable("root") String root, Writer writer) throws IOException {
107-
assertEquals("Invalid path variable value", "42", root);
117+
public void handle(@PathVariable("root") int root, Writer writer) throws IOException {
118+
assertEquals("Invalid path variable value", 42, root);
108119
writer.write("test-" + root);
109120
}
110121

0 commit comments

Comments
 (0)