Details
-
Type:
Improvement
-
Status:
Closed
-
Priority:
Minor
-
Resolution: Fixed
-
Affects Version/s: 1.0.3
-
Fix Version/s: 2.0-RC1
-
Component/s: Controllers
-
Labels:None
-
Environment:Windows XP
Description
Grails defaults to "/controller/action/id" url format. Controller and action names must follow a camelcase convention. I need to change this to recognize a user-friendly URL - having a hyphen as a word separator in controller and action names. For example:
/member-summary/view-subtotals/1
should translate into:
CONTROLLER: MemberSumaryController
ACTION: viewSubtotals
Is there a way of doing this without creating a url mapping for each controller separately in the UrlMappings class?
Thanks,
Ivan
-
- DefaultUrlMappingsHolder.java
- 22/Oct/08 10:48 AM
- 12 kB
- Matt Dertinger
-
- DefaultUrlMappingsHolder.java.patch
- 22/Oct/08 12:57 PM
- 3 kB
- Matt Dertinger
-
Hide
- grails3202.zip
- 10/Oct/11 11:04 AM
- 112 kB
- Jeff Brown
-
- grails3202/.classpath 0.7 kB
- grails3202/.gitignore 0.0 kB
- grails3202/.project 0.5 kB
- grails3202/.../org.codehaus.groovy.eclipse.preferences.prefs 0.1 kB
- grails3202/application.properties 0.1 kB
- grails3202/grails-app/.../BootStrap.groovy 0.1 kB
- grails3202/grails-app/.../BuildConfig.groovy 2 kB
- grails3202/grails-app/conf/Config.groovy 4 kB
- grails3202/grails-app/.../DataSource.groovy 1 kB
- grails3202/grails-app/.../UrlMappings.groovy 0.2 kB
- grails3202/grails-app/.../resources.groovy 0.0 kB
- grails3202/.../MyHyphenatedDemoController.groovy 0.2 kB
- grails3202/.../MyHyphenatedDemo.groovy 0.1 kB
- grails3202/.../messages.properties 3 kB
- grails3202/.../messages_cs_CZ.properties 3 kB
- grails3202/.../messages_da.properties 3 kB
- grails3202/.../messages_de.properties 4 kB
- grails3202/.../messages_es.properties 3 kB
- grails3202/.../messages_fr.properties 2 kB
- grails3202/.../messages_it.properties 2 kB
- grails3202/.../messages_ja.properties 4 kB
- grails3202/.../messages_nl.properties 3 kB
- grails3202/.../messages_pt_BR.properties 3 kB
- grails3202/.../messages_pt_PT.properties 3 kB
- grails3202/.../messages_ru.properties 4 kB
- grails3202/.../messages_sv.properties 3 kB
- grails3202/.../messages_th.properties 5 kB
- grails3202/.../messages_zh_CN.properties 2 kB
- grails3202/grails-app/views/error.gsp 0.3 kB
- grails3202/grails-app/views/index.gsp 3 kB
Activity
- All
- Comments
- Work Log
- History
- Activity
- Git Commits
Is the org.codehaus.groovy.grails.web.mapping.UrlMappingEvaluator a good place to start?
I modified the DefaultUrlMappingsHolder class (see attached file). It converts a uri with hyphens in it to camelCase so that the uri maps to the correct controller and actions. During testing, I noticed that the hyphenated version of the controller or action name doesn't get used when using the scaffolding to generate the views. I was able to workaround this by adding a controller attribute to the g:link and g:form tags and specifying the hyphenated version of the controller and/or action name.
For example:
<g:link class="list" controller="contact-us" action="list">ContactUs List</g:link>
<g:form controller="contact-us" action="save" method="post">
However, in the places where actionSubmit tags are used, this workaround won't work. Do you have any suggestions? And please let me know if you have any feedback on the changes to DefaultMappingsHolder.
Thanks,
Matt
This would be a great boon to having seo friendlier urls. IMO, this should be the default url convention / style if not for this reason alone.
Does the supplied patch aim to change the default, or is it some how intended to be configurable?
Thanks
Although the attach patch is useful, its a breaking change and something configurable is required
The patch also doesn't take into account reverse mapping (ie URL re-writing)
There is no scope / time to resolve these remaining lower priority issues for 1.2 so moving to 1.3
for 1.2 final only issues considered blocking will now be fixed
Here's my crack at the problem (using some of Matt's code), that also includes a way to enable hyphenation through Config.groovy.
- Start with the original 1.3.0RC1 source.
- Open RegexUrlMapping.java
- Add Matt's dashedToCamelCase method
- Add the corresponding camelCaseToDashed method
- Add the following code at the begging of private String createURLInternal(Map parameterValues, String encoding, boolean includeContextPath):
Map<String,Object> config = ConfigurationHolder.getFlatConfig(); if (config != null && Boolean.TRUE.equals(config.get("grails.mapping.hyphenated"))) { if (parameterValues.containsKey(CONTROLLER)) parameterValues.put(CONTROLLER, camelCaseToDashed((String)parameterValues.get(CONTROLLER))); if (parameterValues.containsKey(ACTION)) parameterValues.put(ACTION, camelCaseToDashed((String)parameterValues.get(ACTION))); }
- Add the following code inside private UrlMappingInfo createUrlMappingInfo(String uri, Matcher m) immediately after the code reading *for (Object key : this.parameterValues.keySet())
{params.put(key, this.parameterValues.get(key));}
*:
if (config != null && Boolean.TRUE.equals(config.get("grails.mapping.hyphenated"))) { if (params.containsKey(CONTROLLER)) params.put(CONTROLLER, dashedToCamelCase((String)params.get(CONTROLLER))); if (params.containsKey(ACTION)) params.put(ACTION, dashedToCamelCase((String)params.get(ACTION))); }
- Here's the camelCaseToDashed method code:
public String camelCaseToDashed(String uri) { String patternStr = "([A-Z])"; // Compile regular expression Pattern pattern = Pattern.compile(patternStr); Matcher matcher = pattern.matcher(uri); // Replace all occurrences of pattern StringBuffer sb = new StringBuffer(); boolean matchFound = false; while ((matchFound = matcher.find() ) ) { String replaceStr = "-" + matcher.group().toLowerCase(); matcher.appendReplacement(sb, replaceStr); } matcher.appendTail(sb); // Get result String result = sb.toString(); return result; }
To test URL hyphenation try these steps:
- Create a new app
- Add grails.mapping.hyphenated = true to Config.groovy
- Create a controller HowdyDoodyController and add a booHoo action inside it
- Create a view for the booHoo action and add the following to the GSP: <a href="$
{createLink(controller:'myController',action:'myAction')}
">my link</a>
- Browse to http://localhost:8080/AppName/howdy-doody/boo-hoo
- You should see the booHoo view and a link to /AppName/my-controller/my-action
Another feature that will make URLs cleaner would be the ability to split module names into constituent parts, e.g. to hit a controller named ModuleAUserController you need to enter module-a/user in the URL, or another one named ModuleASectionXUserController would require module-a/section-x/user.
P.S. I'm only just starting with Grails, so, please, pardon any trivial screw-ups on my part.
// Edited to correct the code and make the new functionality controllable via Config.groovy
// Edit: Another code fix
The JavaBean specification provides rules for capitalizing and decapitalizing these things but provides no such rules for other conversions (like hyphens, underscores etc...).
One complication with this is going to be dealing with special cases like class names that contain multiple consecutive upper case letters. For example, if the uri includes something like my-url-helper, the simple converter would turn that into MyUrlHelper. We can't simply go look for a controller class with that name because the class name might be MyURLHelper or MYUrlHelper. Some complexity may be introduced to go do a case-insensitive match on all the candidate classes but that is going to become more complicated when we improve support for controllers in different packages. Hypothetically we could end up with demo.one.MyUrlHelper, demo.two.MYUrlHelper and demo.three.MyURLHelper. What does my-url-helper map to?
I am not proposing that we squash the whole thing because of these issues. We do have to define the behavior for cases like these though.
What about the following mappings?
MyUrlHelper <-> my-url-helper
MyURLHelper <-> my-u-r-l-helper
MYUrlHelper <-> m-y-url-helper
Regardless of the capitalization used, the mapping will always be (as far as I can tell) one-to-one.
By the way, I've been using my patch with no ill effects for 4 months now (running on Grails 1.3.5).
I think it would be better to stick with behaviour that mirrors the JavaBeans specification:
MyUrlHelper <-> my-url-helper (JavaBeans: myUrlHelper) MyURLHelper <-> my-URL-helper (JavaBeans: myURLHelper) MYUrlHelper <-> MY-url-helper (JavaBeans: MYUrlHelper)
with a recommendation that the first class name is the preferred one.
I think avoiding upper-case letters in the URL should be one goal of these clean URLs (each segment consisting of lower-case lettes, numbers and dashes only). Thus, URL segments, such as "my-URL-helper", would not be created if hyphenated URL mappings are enabled. All upper-case letters inside controller and action names would map to a hyphen followed by their lower-case form, except if it's the first letter in the name. The recommendation would be to only capitalize the first letter of acronyms when they are used in controller/action names.
Parts of this have been committed. There is still work to be done related to view rendering and reverse url mapping. This should be functional before M2.
How would the URL -> controller/action mapping work using the UrlConverter interface? I only see a method signature for mapping to a URL.
Ivan,
The converter doesn't really need to know how to convert urls to controller actions. The converter only knows how to convert property names and class names to corresponding url elements. For example, the provided hyphenated converter knows that an action named "someAction" would be represented in a URL as "some-action".
Controllers know their own name and know what actions are available in them. Controllers use the converter to map those action names to URLs. Example:
class MyDemoController {
def renderSomeStuff() {
render 'some stuff'
}
}
That controller would use the converter to generate names like "my-demo" and "render-some-stuff". It then stores information in a Map that maps urls to actions. For example, the above would result in something like this:
uri2methodMap.put("/my-demo/render-some-stuff", "renderSomeStuff")
Then later when a request is made to "/my-demo/render-some-stuff", the controller is asked if it should be associated with that url and the controller knows the answer is "yes" because that url exists as a key in the url2methodMap.
There is more to it than that but that should be enough to explain why the UrlConverter interface doesn't need a conversion method that goes in the other direction (from a url to a controller/action pair). The UrlConvert only needs to know how to convert class and action names into url elements.
Make sense?
Jeff,
Thanks for the thorough explanation; all clear now. And thanks for adding this improvement.
Tried in 2.0M2, it breaks in scaffolding for controllers with two or more words combined
Tried in 2.0M2, it breaks in scaffolding for controllers with two or more words combined
Manuel,
The attached project appears to work. Can you clarify whatever it is that is going wrong?
Thanks for the help.
Manuel,
Apologies. The attached project does not have the appropriate config setting. When I set the config setting to hyphenated I can see the problem now. Now that I can reproduce it, we will get it straightened out.
Thanks for the help!
Hi Jeff,
Sorry for the late reply. Thank you for checking it out; looking forward for the fix because hyphenated urls are more intuitive for me.
Regards,
Manuel
No there isn't but may be a useful feature in the future. Patches welcome