Grails dynamic url mapping - grails

I have a product list I'm trying to create SEO-friendly URLs for.
Example
domain.com/product/cell-phone-razor
"cell-phone-razor" is dynamic from the db
I have successfully achieved this behavior with the following code
"/product/$url"(controller: "product", action:"show")
However there becomes an issue when trying to map other actions non related to the page URL. Example I have an Ajax URL domain.com/product/setPrice which is being mapped to the show action.
I was able to work around this by adding the following to my urlmapping in addition to the previous mapping.
"/product/setPrice" {
controller = "product"
action = "setPrice"
}
Is there a better way to config my url mapping so that I don't need to add rules for every action mapping?

In our application we use the following approach - not saying the perfect one. Basically we're appending URL fragments (that are happily ignored by search engines) to the SEO parts of the URL.
name product: "/$productName-p$productId" {
controller = 'product'
action = 'show'
constraints {
productName matches: '.+'
productId matches: '^[0-9]+$'
}
}
In your case the url should look like domain.com/product/cell-phone-razor-p123455 where 123456 is the product id.

Although not as dynamic as I'd like, but this seems to be the best solution.
"/product/$url"(controller: "product", action:"show") {
constraints {
url(validator: {
return !(it in
['setPriceAction', 'setAnotherAction']
)
})
}
}

Related

Grails <g:link> rewriting links to match UrlMappings instead of controller/action

Currently using grails 4.0.3.
I'm trying to generate a link to a controller action using a simple <g:link> tag:
<g:link controller="myController" action="myAction" params="[someParam:'myValue']">Link text</g:link>
In my UrlMappings file, I have this controller mapped to a common URL for external calls. This mapping forces some parameters that I want forced when coming to this mapping:
class UrlMappings {
static mappings = {
"/service/someExposedUrl" {
controller = "myController"
action = "myAction"
someParam = "defaultValue"
}
}
}
However, the link that gets written to the page in my application gets written using the UrlMapping definition.
http://serverUrl/service/someExposedUrl?someParam=<ignored>
instead of
http://serverUrl/myController/myAction?someParam=myValue
In this case, the parameters are ignored on the /service URL because they're hard-coded in the UrlMapping.
Is there a way to force grails to link to the specified controller/action instead of the mapping?
Up front caveat: I have not tested this for this specific situation, though I have used these pieces individually. Second caveat: I would not be remotely surprised to learn there's an even better way to do this...grails has a lot of options sometimes!
You should be able to create a named mapping in your UrlMappings, then reference that in your createLink.
UrlMappings:
name arbitraryName: "/myController/myAction" {
controller = "myController"
action = "myAction"
}
Link:
<g:link mapping="arbitraryName" params="[someParam:'myValue']">Link text</g:link>
You may be able to get what you need with a combination of Custom URL mapping and the exclude keyword in the original URLMappings.
Create MyControllerUrlMappings. This guide gives several examples of different ways of writing them out. I found it helpful.
class MyControllerUrlMappings {
static mappings = {
// Url Mappings specific only to this controller
}
}
https://guides.grails.org/grails_url_mappings/guide/index.html#_multiple_urlmappings_files
Your app will still be running the original UrlMappings file though. So you'll have the new custom Url mappings and the default ones. To fix that you can exclude your controller and it's methods in the original UrlMappings file.
class UrlMappings {
static excludes = [
"/myController",
"/myController/*"
]
static mappings = {...}
...
Not as elegant as I'd like, but in a similar situation this is how I got it working.

Grails: URL mapping with .gsp extension/format

I have a url request like www.xyz.com/customer/list.gsp
When I try to map the url to remove .gsp:
"/customer/list.gsp"(controller: "customer") {
action = "list"
}
grails application won't recognize the url and is throwing 404 error. Am I missing something here?
If you want to remove .gsp from the url then you can use a mapping like this...
"/customer/list"(controller: "customer") {
action = "list"
}
You could also do this...
"/customer/list"(controller: "customer", action: "list")
If you want 1 mapping for all the actions in the controller, you could do this:
"/customer/$action"(controller: "customer")
The default generated mapping includes "/$controller/$action" which allows you map to any action in any controller.
With any of that, sending a request to /customer/list would work.
Update: apparently it is fine to map to GSPs. I still think that the info below may be helpful so I'm leaving the answer up, but perhaps I have misunderstood your question.
Original response:
You shouldn't be mapping to or requesting gsps at all. They're used to generate views, but are not viewable without rendering.
Instead, go to url like www.xyz.com/customer/list and map that like
"/customer/list" (controller: "customer") {
action = "list"
}
Or even better, you don't need a custom mapping for each endpoint. A default like this will work:
"/$controller/$action?/$id?" { }
Your CustomerController will render the list.gsp in the list action.

Grails Reverse Url Mapping: Is there a way to build links based on the currently matched route?

I'm trying to build some dynamic URL mappings that are based on the domain classes of the grails project. The goal is to use a single generic controller, but the URL decides which domain is being used for processing. The problem is now, that I don't get the desired URLs when using the <g:link /> tag.
I tried the following variants to create the URL mappings:
static mappings = {
Holders.grailsApplication.domainClasses.each {
"/admin/${it.propertyName}/$action?/$id?"(controller: "admin")
}
}
static mappings = {
"/admin/$domainClass/$action?/$id?"(controller: "admin")
}
Both variants work for the actual URL matching. But I personally don't like the behavior of the reverse URL mapping by grails. In case of variant 1, the reverse mapping always resolves to the last added URL mapping for the AdminController. For case 2 I have the problem that I would have to pass the domainClass-Parameter to every link-creating call, even though it is theoretically not necessary, since the information is already present in the current request.
I know there is the possibility of using named URL mappings and then using something like <g:link mapping="mapping_name" />. The problem is that I am using some generic application-wide partial-views, where I try to only provide the necessary information for creating a link, like <g:link action="something" />.
This leads to my two questions:
Is there a possibility to get g:link to build the URL based on the matched mapping in the current request?
Is there a way to get a reference to the mapping that matched the current request, so I can implement to desired behavior myself?
You could define named mappings like
Holders.grailsApplication.domainClasses.each { dc ->
name((dc.propertyName):"/admin/${dc.propertyName}/$action?/$id?" {
controller = "admin"
domainClass = dc.propertyName
})
}
With the mapping name saved in the params you can now do
<g:link mapping="${params.domainClass}">link text</g:link>

How to create canocical from controller in grails?

In grails, I have a link like /myapp/questions/all
The all is a parameter (all, replied, ...) passed to my controller.
I have a form to search question depending of type : in all, in replied, ...
In the search form, I have an hidden field to pass parameter.
But the url displayed is /myapp/questions/ ans not /myapp/questions/all
So I tried with url : url="[action:'question', controller:'mycontroller', params:['monparam':'${mavariable}']]"
but it's not working.
Any idea ?
Thanks
You can do it like this:
class UrlMappings {
static mappings = {
name nameOfTheMapping: "/question/$para/" {
controller = "mycontroller"
action = "question"
}
...
Then you can access the mapping by:
<a href='${createLink(mapping: 'nameOfTheMapping', params: [para: para.encodeAsUrl()])}' title='test'>Test</a>
The above code is created in my taglib, so it maybe a little different if you want to use it in a view.
I don't completely understand your question, but it seems you are not following the grails convention. the url is of the form
/app/controller/action
so grails is interpreting the 'all' part of your url as the action to invoke on the questions controller (what I got from your 'link like /myapp/questions/all').
Where I got confused was with your url specification.
url="[action:'question', controller:'mycontroller', params:['monparam':'${mavariable}']]"
Based on that, you should have a controller called 'mycontroller', with an action called 'question' on it. The url you will see in the browser would be
/app/mycontroller/question?monparam:whatever
See here for details on controllers in general.
You need to edit grails-app/conf/UrlMappings.groovy and create a mapping to the controller that omits the action. (since you're handling this all within one action)
something like
"/questions/$question_type" (controller: 'questions', action: 'your_action')
where "your_action" is the name of the action that is processing these requests.
Then in QuestionsController.groovy:
def your_action = {
// use question_type as needed
def questions = Questions.findByQuestionType(params.question_type)
// etc.
}
You can do a variety of things to affect the mapping of urls to requests, check out the UrlMapping section in the Grails User Guide.

Refactoring my route to be more dynamic

Currently my URL structure is like this:
www.example.com/honda/
www.example.com/honda/add
www.example.com/honda/29343
I have a controller named HondaController.
Now I want to refactor this so I can support more car manufacturers.
The database has a table that stores all the manufacturers that I want to support.
How can I keep my URL like above, but now support:
www.example.com/ford
www.example.com/toyota/add
etc.
I can easily rename the HondaController to CarController, and just pass in the string 'honda' or 'toyota' and my controller will work (it is hard coded to 'honda' right now).
Is this possible? I'm not sure how how to make a route dynamic based on what I have in the database.
Any part of your route can be dynamic just be making it into a route parameter. So instead of "/honda/{action}", do:
/{manufacturer}/{action}
This will give you a parameter called "manufacturer" that was passed to your action method. So your action method signature could now be:
public ActionResult add(string manufacturer) { }
It would be up to you to verify that the manufacturer parameter correctly matched the list of manufacturers in the database - it would probably be best to cache this list for a quicker lookup.
Updated: What I mean by "you have to take out the default parameters" for the default route is this. If you have:
route.MapRoute("Default", "/{controller}/{action}/{id}",
new { id = 1 } // <-- this is the parameter default
);
then this route will match any url with two segments, as well as any url with three segments. So "/product/add/1" will be handled by this route, but so will "/product/add".
If you take out the "new { id = 1 }" part, it will only handle URL's that look like "/product/add/1".
i have made something like this for granite as i wanted to have a material controller but have a url like so:
black/granite/worktops
black/quartz/worktops
etc
i did this route:
routes.MapRoute("Quote", "quote/{color}/{surface}/{type}",
new {controller = "Quote", action = "surface"});
swap quote for car so u can have:
car/honda/accord
your route can then be
routes.MapRoute("cars", "car/{make}/{model}",
new {controller = "Cars", action = "Index"});
your actionResults can then look like this:
public ActionResult Index(string make, string model)
{
//logic here to get where make and model
return View();
}
that i think covers it
What I recommend is instead using:
domain/m/<manufacturer>/<action>
Where 'm' is the manufacturer controller. This will allow you to use the same controller for all of your extensions and save you a lot of headache in the future, especially when adding new features. Using a one-letter controller is often times desirable when you want to retain your first variable ( in this case) as the first point of interest.

Resources