Skip to content

Commit 7cbcecd

Browse files
committed
UrlMappings - use non-greedy matching for URL paths. Fixes #9849
Previously Grails would use greedy matches between paths to math up until each slash. This causes problem with overlapping URL mappings (see #9849). This changes makes paths non-greedy and groups for the format extension mapping into a single optional group, thus fixing the problem.
1 parent e2ad69d commit 7cbcecd

File tree

2 files changed

+78
-10
lines changed

2 files changed

+78
-10
lines changed

grails-web-url-mappings/src/main/groovy/org/grails/web/mapping/RegexUrlMapping.java

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@
7474
@SuppressWarnings("rawtypes")
7575
public class RegexUrlMapping extends AbstractUrlMapping {
7676

77+
public static final String FORMAT_PARAMETER = "format";
7778
private Pattern[] patterns;
7879
private Map<Integer, List<Pattern>> patternByTokenCount = new HashMap<Integer, List<Pattern>>();
7980
private UrlMappingData urlData;
@@ -251,9 +252,9 @@ protected Pattern convertToRegex(String url) {
251252

252253
// Now replace "*" with "[^/]" and "**" with ".*".
253254
pattern = "^" + urlRoot
254-
.replace("(\\.(*))", "\\.?([^/]+)?")
255-
.replaceAll("([^\\*])\\*([^\\*])", "$1[^/]+$2")
256-
.replaceAll("([^\\*])\\*$", "$1[^/]+")
255+
.replace("(\\.(*))", "(\\.[^/]+)?")
256+
.replaceAll("([^\\*])\\*([^\\*])", "$1[^/]+?$2")
257+
.replaceAll("([^\\*])\\*$", "$1[^/]+?")
257258
.replaceAll("\\*\\*", ".*");
258259

259260
if("/(*)(\\.(*))".equals(urlEnd)) {
@@ -263,12 +264,12 @@ protected Pattern convertToRegex(String url) {
263264
pattern += "/([^/]+)\\.([^/.]+)?";
264265
} else {
265266
pattern += urlEnd
266-
.replace("(\\.(*))", "\\.?([^/]+)?")
267-
.replaceAll("([^\\*])\\*([^\\*])", "$1[^/]+$2")
268-
.replaceAll("([^\\*])\\*$", "$1[^/]+")
267+
.replace("(\\.(*))", "(\\.[^/]+)?")
268+
.replaceAll("([^\\*])\\*([^\\*])", "$1[^/]+?$2")
269+
.replaceAll("([^\\*])\\*$", "$1[^/]+?")
269270
.replaceAll("\\*\\*", ".*")
270-
.replaceAll("\\(\\[\\^\\/\\]\\+\\)\\\\\\.", "([^/.]+)\\\\.")
271-
.replaceAll("\\(\\[\\^\\/\\]\\+\\)\\?\\\\\\.", "([^/.]+)\\?\\\\.")
271+
.replaceAll("\\(\\[\\^\\/\\]\\+\\)\\\\\\.", "([^/.]+?)\\\\.")
272+
.replaceAll("\\(\\[\\^\\/\\]\\+\\)\\?\\\\\\.", "([^/.]+?)\\?\\\\.")
272273
;
273274
}
274275
pattern += "/??$";
@@ -634,7 +635,13 @@ private UrlMappingInfo createUrlMappingInfo(String uri, Matcher m) {
634635
return null;
635636
}
636637

637-
params.put(cp.getPropertyName(), lastGroup);
638+
String propertyName = cp.getPropertyName();
639+
if(lastGroup != null) {
640+
if(FORMAT_PARAMETER.equals(propertyName) && lastGroup.startsWith(".")) {
641+
lastGroup = lastGroup.substring(1);
642+
}
643+
}
644+
params.put(propertyName, lastGroup);
638645
break;
639646
}
640647
else {
@@ -651,7 +658,11 @@ private UrlMappingInfo createUrlMappingInfo(String uri, Matcher m) {
651658
return null;
652659
}
653660

654-
params.put(cp.getPropertyName(), lastGroup);
661+
String propertyName = cp.getPropertyName();
662+
if(FORMAT_PARAMETER.equals(propertyName) && lastGroup.startsWith(".")) {
663+
lastGroup = lastGroup.substring(1);
664+
}
665+
params.put(propertyName, lastGroup);
655666
}
656667
}
657668

grails-web-url-mappings/src/test/groovy/org/codehaus/groovy/grails/web/mapping/RestfulResourceMappingSpec.groovy

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,63 @@ import spock.lang.Specification
1919
* @author Graeme Rocher
2020
*/
2121
class RestfulResourceMappingSpec extends Specification{
22+
@Issue('https://github.com/grails/grails-core/issues/9849')
23+
void "Test conflicting UrlMappings related to a resource mappings"() {
24+
given:"A URL mappings definition with a single resource"
25+
def urlMappingsHolder = getUrlMappingsHolder {
26+
"/api/worksheet"(resources:"worksheet")
27+
"/api/work"(resources:"work")
28+
}
29+
30+
when:"The URLs are obtained"
31+
def urlMappings = urlMappingsHolder.urlMappings
32+
33+
then:"There are correct of them in total"
34+
urlMappings.size() == 16
35+
36+
expect:"That the appropriate URLs are matched for the appropriate HTTP methods"
37+
urlMappingsHolder.matchAll('/api/work', 'GET')
38+
urlMappingsHolder.matchAll('/api/work', 'GET')[0].controllerName == 'work'
39+
40+
urlMappingsHolder.matchAll('/api/work.xml', 'GET')
41+
urlMappingsHolder.matchAll('/api/work.xml', 'GET')[0].controllerName == 'work'
42+
urlMappingsHolder.matchAll('/api/work.xml', 'GET')[0].parameters.format == 'xml'
43+
44+
urlMappingsHolder.matchAll('/api/worksheet', 'GET')
45+
urlMappingsHolder.matchAll('/api/worksheet', 'GET')[0].controllerName == 'worksheet'
46+
47+
urlMappingsHolder.matchAll('/api/worksheet.xml', 'GET')
48+
urlMappingsHolder.matchAll('/api/worksheet.xml', 'GET')[0].controllerName == 'worksheet'
49+
urlMappingsHolder.matchAll('/api/worksheet.xml', 'GET')[0].parameters.format == 'xml'
50+
51+
}
52+
53+
@Issue('https://github.com/grails/grails-core/issues/9877')
54+
void "Test conflicting UrlMappings with default url mapping"() {
55+
given:"A URL mappings definition with a single resource"
56+
def urlMappingsHolder = getUrlMappingsHolder {
57+
"/$controller/$action?/$id?(.$format)?"{
58+
constraints {
59+
// apply constraints here
60+
}
61+
}
62+
63+
"/foo"(resources: 'foo')
64+
}
65+
66+
when:"The URLs are obtained"
67+
def urlMappings = urlMappingsHolder.urlMappings
68+
69+
then:"There are correct of them in total"
70+
urlMappings.size() == 9
71+
72+
expect:"That the appropriate URLs are matched for the appropriate HTTP methods"
73+
urlMappingsHolder.matchAll('/foo', 'GET')
74+
urlMappingsHolder.matchAll('/foo', 'GET')[0].controllerName == 'foo'
75+
76+
urlMappingsHolder.matchAll('/fooBar', 'GET')[0].parameters.controller == 'fooBar'
77+
urlMappingsHolder.matchAll('/fooWithAnythingAfterIt', 'GET')[0].parameters.controller == 'fooWithAnythingAfterIt'
78+
}
2279

2380
void "Test resource members"() {
2481
given:

0 commit comments

Comments
 (0)