@@ -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 ) {
0 commit comments