Skip to content

Commit f0d4735

Browse files
committed
Fixes #9480 and #9479 - URL mappings improvements
1 parent ea2b9e0 commit f0d4735

File tree

3 files changed

+263
-12
lines changed

3 files changed

+263
-12
lines changed

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

Lines changed: 199 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,7 @@ class UrlMappingBuilder extends GroovyObjectSupport {
229229
private Object parseRequest;
230230
private Deque<ParentResource> parentResources = new ArrayDeque<ParentResource>();
231231
private Deque<MetaMappingInfo> mappingInfoDeque = new ArrayDeque<MetaMappingInfo>();
232+
private boolean isInCollection;
232233

233234
public UrlMappingBuilder(Binding binding) {
234235
this.binding = binding;
@@ -392,6 +393,185 @@ Object propertyMissing(String name) {
392393
return parameterValues.get(name);
393394
}
394395

396+
/**
397+
* Matches the GET method
398+
*
399+
* @param arguments The arguments
400+
* @param uri The URI
401+
* @param callable the customizer
402+
* @return the UrlMapping
403+
*/
404+
public UrlMapping get(Map arguments, String uri, Closure callable) {
405+
arguments.put(UrlMapping.HTTP_METHOD, HttpMethod.GET.toString());
406+
return (UrlMapping) _invoke(uri, new Object[]{ arguments, callable }, this);
407+
}
408+
public UrlMapping get(Map arguments, String uri) {
409+
return get(arguments, uri, null);
410+
}
411+
public UrlMapping get(RegexUrlMapping regexUrlMapping) {
412+
urlMappings.remove(regexUrlMapping);
413+
RegexUrlMapping newMapping = new RegexUrlMapping(regexUrlMapping, HttpMethod.GET);
414+
urlMappings.add(newMapping);
415+
return newMapping;
416+
}
417+
418+
419+
/**
420+
* Matches the POST method
421+
*
422+
* @param arguments The arguments
423+
* @param uri The URI
424+
* @return the UrlMapping
425+
*/
426+
public UrlMapping post(Map arguments, String uri, Closure callable) {
427+
arguments.put(UrlMapping.HTTP_METHOD, HttpMethod.POST);
428+
return (UrlMapping) _invoke(uri, new Object[]{ arguments, callable }, this);
429+
}
430+
public UrlMapping post(Map arguments, String uri) {
431+
return post(arguments, uri, null);
432+
}
433+
public UrlMapping post(RegexUrlMapping regexUrlMapping) {
434+
urlMappings.remove(regexUrlMapping);
435+
RegexUrlMapping newMapping = new RegexUrlMapping(regexUrlMapping, HttpMethod.POST);
436+
urlMappings.add(newMapping);
437+
return newMapping;
438+
}
439+
440+
/**
441+
* Matches the PUT method
442+
*
443+
* @param arguments The arguments
444+
* @param uri The URI
445+
* @return the UrlMapping
446+
*/
447+
public UrlMapping put(Map arguments, String uri, Closure callable) {
448+
arguments.put(UrlMapping.HTTP_METHOD, HttpMethod.PUT);
449+
return (UrlMapping) _invoke(uri, new Object[]{ arguments, callable }, this);
450+
}
451+
public UrlMapping put(Map arguments, String uri) {
452+
return put(arguments, uri, null);
453+
}
454+
public UrlMapping put(RegexUrlMapping regexUrlMapping) {
455+
urlMappings.remove(regexUrlMapping);
456+
RegexUrlMapping newMapping = new RegexUrlMapping(regexUrlMapping, HttpMethod.PUT);
457+
urlMappings.add(newMapping);
458+
return newMapping;
459+
}
460+
461+
/**
462+
* Matches the PATCH method
463+
*
464+
* @param arguments The arguments
465+
* @param uri The URI
466+
* @return the UrlMapping
467+
*/
468+
public UrlMapping patch(Map arguments, String uri, Closure callable) {
469+
arguments.put(UrlMapping.HTTP_METHOD, HttpMethod.PATCH);
470+
return (UrlMapping) _invoke(uri, new Object[]{ arguments, callable }, this);
471+
}
472+
public UrlMapping patch(Map arguments, String uri) {
473+
return patch(arguments, uri, null);
474+
}
475+
public UrlMapping patch(RegexUrlMapping regexUrlMapping) {
476+
urlMappings.remove(regexUrlMapping);
477+
RegexUrlMapping newMapping = new RegexUrlMapping(regexUrlMapping, HttpMethod.PATCH);
478+
urlMappings.add(newMapping);
479+
return newMapping;
480+
}
481+
482+
/**
483+
* Matches the PATCH method
484+
*
485+
* @param arguments The arguments
486+
* @param uri The URI
487+
* @return the UrlMapping
488+
*/
489+
public UrlMapping delete(Map arguments, String uri, Closure callable) {
490+
arguments.put(UrlMapping.HTTP_METHOD, HttpMethod.DELETE);
491+
return (UrlMapping) _invoke(uri, new Object[]{ arguments, callable }, this);
492+
}
493+
public UrlMapping delete(Map arguments, String uri) {
494+
return delete(arguments, uri, null);
495+
}
496+
public UrlMapping delete(RegexUrlMapping regexUrlMapping) {
497+
urlMappings.remove(regexUrlMapping);
498+
RegexUrlMapping newMapping = new RegexUrlMapping(regexUrlMapping, HttpMethod.DELETE);
499+
urlMappings.add(newMapping);
500+
return newMapping;
501+
}
502+
/**
503+
* Matches the HEAD method
504+
*
505+
* @param arguments The arguments
506+
* @param uri The URI
507+
* @return the UrlMapping
508+
*/
509+
public UrlMapping head(Map arguments, String uri, Closure callable) {
510+
arguments.put(UrlMapping.HTTP_METHOD, HttpMethod.HEAD);
511+
return (UrlMapping) _invoke(uri, new Object[]{ arguments, callable }, this);
512+
}
513+
public UrlMapping head(Map arguments, String uri) {
514+
return head(arguments, uri, null);
515+
}
516+
public UrlMapping head(RegexUrlMapping regexUrlMapping) {
517+
urlMappings.remove(regexUrlMapping);
518+
RegexUrlMapping newMapping = new RegexUrlMapping(regexUrlMapping, HttpMethod.HEAD);
519+
urlMappings.add(newMapping);
520+
return newMapping;
521+
}
522+
523+
/**
524+
* Matches the HEAD method
525+
*
526+
* @param arguments The arguments
527+
* @param uri The URI
528+
* @return the UrlMapping
529+
*/
530+
public UrlMapping options(Map arguments, String uri, Closure callable) {
531+
arguments.put(UrlMapping.HTTP_METHOD, HttpMethod.OPTIONS);
532+
return (UrlMapping) _invoke(uri, new Object[]{ arguments, callable }, this);
533+
}
534+
public UrlMapping options(Map arguments, String uri) {
535+
return options(arguments, uri, null);
536+
}
537+
public UrlMapping options(RegexUrlMapping regexUrlMapping) {
538+
urlMappings.remove(regexUrlMapping);
539+
RegexUrlMapping newMapping = new RegexUrlMapping(regexUrlMapping, HttpMethod.OPTIONS);
540+
urlMappings.add(newMapping);
541+
return newMapping;
542+
}
543+
/**
544+
* Define Url mapping collections that are nested directly below the parent resource (without the id)
545+
*
546+
* @param callable The callable
547+
*/
548+
public void collection(Closure callable) {
549+
boolean previousState = isInCollection;
550+
this.isInCollection = true;
551+
try {
552+
callable.setDelegate(this);
553+
callable.call();
554+
} finally {
555+
isInCollection = previousState ;
556+
}
557+
}
558+
559+
/**
560+
* Define Url mapping members that are nested directly below the parent resource and resource id
561+
*
562+
* @param callable The callable
563+
*/
564+
public void members(Closure callable) {
565+
boolean previousState = isInCollection;
566+
this.isInCollection = false;
567+
try {
568+
callable.setDelegate(this);
569+
callable.call();
570+
} finally {
571+
isInCollection = previousState ;
572+
}
573+
}
574+
395575
private Object _invoke(String methodName, Object arg, Object delegate) {
396576
try {
397577
MetaMappingInfo mappingInfo = pushNewMetaMappingInfo();
@@ -403,8 +583,9 @@ private Object _invoke(String methodName, Object arg, Object delegate) {
403583
// Create a new parameter map for this mapping.
404584
parameterValues = new HashMap<String, Object>();
405585
Map variables = binding != null ? binding.getVariables() : null;
586+
boolean hasParent = !parentResources.isEmpty();
406587
try {
407-
if (parentResources.isEmpty()) {
588+
if (!hasParent) {
408589
urlDefiningMode = false;
409590
}
410591
args = args != null && args.length > 0 ? args : new Object[]{Collections.EMPTY_MAP};
@@ -497,7 +678,8 @@ private Object _invoke(String methodName, Object arg, Object delegate) {
497678
if (controller != null) {
498679
createResourceRestfulMappings(controllerName, mappingInfo.getPlugin(), mappingInfo.getNamespace(), version, urlData, currentConstraints, calculateIncludes(namedArguments, DEFAULT_RESOURCES_INCLUDES));
499680
}
500-
} else {
681+
}
682+
else {
501683

502684
invokeLastArgumentIfClosure(args);
503685
UrlMapping urlMapping = getURLMappingForNamedArgs(namedArguments, urlData, mappedURI, isResponseCode, currentConstraints);
@@ -510,7 +692,7 @@ private Object _invoke(String methodName, Object arg, Object delegate) {
510692
if (binding != null) {
511693
variables.clear();
512694
}
513-
if (parentResources.isEmpty()) {
695+
if (!hasParent) {
514696
urlDefiningMode = true;
515697
}
516698
}
@@ -592,8 +774,12 @@ private String establishFullURI(String uri, List<ConstrainedProperty> constraine
592774
uriBuilder.append(parentResource.uri);
593775
} else {
594776
if (parentResource.controllerName != null) {
595-
uriBuilder.append(parentResource.uri).append(SLASH).append(CAPTURING_WILD_CARD);
596-
constrainedList.add(new ConstrainedProperty(UrlMapping.class, parentResource.controllerName + "Id", String.class));
777+
uriBuilder.append(parentResource.uri);
778+
779+
if(!isInCollection) {
780+
uriBuilder.append(SLASH).append(CAPTURING_WILD_CARD);
781+
constrainedList.add(new ConstrainedProperty(UrlMapping.class, parentResource.controllerName + "Id", String.class));
782+
}
597783
}
598784
}
599785

@@ -936,6 +1122,7 @@ private UrlMapping getURLMappingForNamedArgs(Map namedArguments,
9361122
}
9371123

9381124
private Object getVariableFromNamedArgsOrBinding(Map namedArguments, Map bindingVariables, String variableName, Object defaultValue) {
1125+
9391126
Object returnValue;
9401127
returnValue = namedArguments.get(variableName);
9411128
if (returnValue == null) {
@@ -953,7 +1140,13 @@ private Object getParseRequest(Map namedArguments, Map bindingVariables) {
9531140
}
9541141

9551142
private Object getControllerName(Map namedArguments, Map bindingVariables) {
956-
return getVariableFromNamedArgsOrBinding(namedArguments, bindingVariables, GrailsControllerClass.CONTROLLER, getMetaMappingInfo().getController());
1143+
Object fromBinding = getVariableFromNamedArgsOrBinding(namedArguments, bindingVariables, GrailsControllerClass.CONTROLLER, getMetaMappingInfo().getController());
1144+
if(fromBinding == null && !parentResources.isEmpty()) {
1145+
return parentResources.peekLast().controllerName;
1146+
}
1147+
else {
1148+
return fromBinding;
1149+
}
9571150
}
9581151

9591152
private Object getPluginName(Map namedArguments, Map bindingVariables) {

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import org.grails.web.servlet.mvc.GrailsWebRequest;
5050
import org.grails.web.servlet.mvc.exceptions.ControllerExecutionException;
5151
import org.grails.web.util.WebUtils;
52+
import org.springframework.http.HttpMethod;
5253
import org.springframework.util.Assert;
5354
import org.springframework.validation.Errors;
5455
import org.springframework.validation.MapBindingResult;
@@ -96,7 +97,9 @@ public RegexUrlMapping(UrlMappingData data, URI uri, ConstrainedProperty[] const
9697
public RegexUrlMapping(UrlMappingData data, Object controllerName, Object actionName, Object namespace, Object pluginName, Object viewName, String httpMethod, String version, ConstrainedProperty[] constraints, GrailsApplication grailsApplication) {
9798
this(null, data, controllerName, actionName, namespace, pluginName, viewName, httpMethod, version, constraints, grailsApplication);
9899
}
99-
100+
public RegexUrlMapping(RegexUrlMapping regexUrlMapping, HttpMethod httpMethod) {
101+
this(regexUrlMapping.urlData, regexUrlMapping.controllerName, regexUrlMapping.actionName, regexUrlMapping.namespace, regexUrlMapping.pluginName, regexUrlMapping.viewName, httpMethod.toString(), regexUrlMapping.version, regexUrlMapping.constraints, regexUrlMapping.grailsApplication);
102+
}
100103
/**
101104
* Constructs a new RegexUrlMapping for the given pattern, controller name, action name and constraints.
102105
*

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

Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import org.grails.web.mapping.DefaultLinkGenerator
66
import org.grails.web.mapping.DefaultUrlMappingEvaluator
77
import org.grails.web.mapping.DefaultUrlMappingsHolder
88

9-
import static org.springframework.http.HttpMethod.*
9+
//import static org.springframework.http.HttpMethod.*
1010
import grails.web.CamelCaseUrlConverter
11-
11+
import org.springframework.http.HttpMethod
1212
import org.springframework.mock.web.MockServletContext
1313

1414
import spock.lang.Ignore
@@ -19,7 +19,62 @@ import spock.lang.Specification
1919
* @author Graeme Rocher
2020
*/
2121
class RestfulResourceMappingSpec extends Specification{
22-
22+
23+
void "Test resource members"() {
24+
given:
25+
def urlMappingsHolder = getUrlMappingsHolder {
26+
"/books"(resources: "book") {
27+
get "/preview", action:"preview"
28+
collection {
29+
get "/about", action:"about"
30+
}
31+
}
32+
}
33+
34+
expect:
35+
urlMappingsHolder.matchAll("/books/1/preview", HttpMethod.GET).iterator().next().actionName == 'preview'
36+
urlMappingsHolder.matchAll("/books/about", HttpMethod.GET).iterator().next().actionName == 'about'
37+
}
38+
39+
void "Test using method name prefixes"() {
40+
given:
41+
def urlMappingsHolder = getUrlMappingsHolder {
42+
get "/books", controller:"book"
43+
get "/books/$id", controller:"book", action:'show'
44+
post "/books", controller:"book", action:'save'
45+
patch "/books", controller:"book", action:'update'
46+
delete "/books", controller:"book", action:'delete'
47+
48+
49+
get "/moreBooks"(controller:"book")
50+
post "/moreBooks"(controller:"book", action:'save') {
51+
52+
}
53+
patch "/moreBooks"(controller:"book", action:'update') {
54+
55+
}
56+
put "/moreBooks"(controller:"book", action:'update') {
57+
58+
}
59+
delete "/moreBooks"(controller:"book", action:'delete') {
60+
61+
}
62+
}
63+
64+
expect:
65+
urlMappingsHolder.match("/books")
66+
urlMappingsHolder.matchAll("/books/1", HttpMethod.GET).iterator().next().actionName == 'show'
67+
urlMappingsHolder.matchAll("/books", HttpMethod.POST).iterator().next().actionName == 'save'
68+
urlMappingsHolder.matchAll("/books", HttpMethod.PATCH).iterator().next().actionName == 'update'
69+
urlMappingsHolder.matchAll("/books", HttpMethod.DELETE).iterator().next().actionName == 'delete'
70+
71+
urlMappingsHolder.match("/moreBooks")
72+
urlMappingsHolder.matchAll("/moreBooks", HttpMethod.POST).iterator().next().actionName == 'save'
73+
urlMappingsHolder.matchAll("/moreBooks", HttpMethod.PUT).iterator().next().actionName == 'update'
74+
urlMappingsHolder.matchAll("/moreBooks", HttpMethod.PATCH).iterator().next().actionName == 'update'
75+
urlMappingsHolder.matchAll("/moreBooks", HttpMethod.DELETE).iterator().next().actionName == 'delete'
76+
}
77+
2378
@Issue('GRAILS-11748')
2479
void 'Test params.action'() {
2580
given:
@@ -318,8 +373,8 @@ class RestfulResourceMappingSpec extends Specification{
318373
urlMappings.size() == 9
319374

320375
expect:"That the appropriate URLs are matched for the appropriate HTTP methods"
321-
urlMappingsHolder.allowedMethods('/books') == [POST, GET] as Set
322-
urlMappingsHolder.allowedMethods('/books/1') == [GET, DELETE, PUT, PATCH] as Set
376+
urlMappingsHolder.allowedMethods('/books') == [HttpMethod.POST, HttpMethod.GET] as Set
377+
urlMappingsHolder.allowedMethods('/books/1') == [HttpMethod.GET, HttpMethod.DELETE, HttpMethod.PUT, HttpMethod.PATCH] as Set
323378
urlMappingsHolder.matchAll('/books', 'GET')
324379
urlMappingsHolder.matchAll('/books', 'GET')[0].actionName == 'index'
325380
urlMappingsHolder.matchAll('/books', 'GET')[0].httpMethod == 'GET'

0 commit comments

Comments
 (0)