Flexible URL routing
Gaelyk provides a flexible and powerful URL routing system: you can use a small Groovy Domain-Specific Language for defining routes for nicer and friendlier URLs.
Configuring URL routing
To enable the URL routing system, you should configure the RoutesFilter
servlet filter in web.xml
:
... <filter> <filter-name>RoutesFilter</filter-name> <filter-class>groovyx.gaelyk.routes.RoutesFilter</filter-class> </filter> ... <filter-mapping> <filter-name>RoutesFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> ...
Note: We advise to setup only one route filter, but it is certainly possible to define several ones for different areas of your site. By default, the filter is looking for the fileWEB-INF/routes.groovy
for the routes definitions, but it is possible to override this setting by specifying a different route DSL file with a servlet filter configuration parameter:<filter> <filter-name>RoutesFilter</filter-name> <filter-class>groovyx.gaelyk.routes.RoutesFilter</filter-class> <init-param> <param-name>routes.location</param-name> <param-value>WEB-INF/blogRoutes.groovy</param-value> </init-param> </filter>
Warning: The filter is stopping the chain filter once a route is found. So you should ideally put the route filter as the last element of the chain.
Defining URL routes
By default, once the filter is configured, URL routes are defined in WEB-INF/routes.groovy
,
in the form of a simple Groovy scripts, defining routes in the form of a lightweight DSL.
The capabilities of the routing system are as follow, you can:
- match requests made with a certain method (GET, POST, PUT, DELETE), or all
- define the final destination of the request
- chose whether you want to forward or redirect to the destination URL (i.e. URL rewriting through forward vs. redirection)
- express variables in the route definition and reuse them as variables in the final destination of the request
- validate the variables according to some boolean expression, or regular expression matching
- use the available GAE services in the script (for instance, creating routes from records from the datastore)
- cache the output of groovlets and templates pointed by that route for a specified period of time
- specify a handler for incoming email messages
- specify a handler for incoming jabber messages
Let's see those various capabilities in action.
Imagine we want to define friendly URLs for our blog application.
Let's configure a first route in WEB-INF/routes.groovy
.
Say you want to provide a shorthand URL /about
that would redirect to your first blog post.
You could configure the /about
route for all GET requests calling the get
method.
You would then redirect those requests to the final destination with the redirect
named argument:
get "/about", redirect: "/blog/2008/10/20/welcome-to-my-blog"
If you prefer to do a forward, so as to do URL rewriting to keep the nice short URL,
you would just replace redirect
with forward
as follows:
get "/about", forward: "/blog/2008/10/20/welcome-to-my-blog"
Original URI which was accessed such as /about
can be found using request.originalURI
attribute
Note: In addition toredirect
andforward
, you can also do aredirect301
, which is a permanent redirect.
If you have different routes for different HTTP methods, you can use the get
, post
,
put
and delete
methods.
If you want to catch all the requests independently of the HTTP method used, you can use the all
function.
Another example, if you want to post only to a URL to create a new blog article,
and want to delegate the work to a post.groovy
Groovlet, you would create a route like this one:
post "/new-article", forward: "/post.groovy" // shortcut for "/WEB-INF/groovy/post.groovy"
Note: When running your applications in development mode, Gaelyk is configured to take into accounts
any changes made to the routes.groovy
definition file.
Each time a request is made, which goes through the route servlet filter, Gaelyk checks whether a more
recent route definition file exists.
However, once deployed on the Google App Engine cloud, the routes are set in stone and are not reloaded.
The sole cost of the routing system is the regular expression mapping to match request URIs against route patterns.
Incoming email and jabber messages
Two special routing rules exist for defining handlers dedicated to receiving incoming email messages and jabber messages.
email to: "/receiveEmail.groovy" jabber to: "/receiveJabber.groovy"
jabber chat, to: "/receiveJabber.groovy" // synonym of jabber to: "..." // for Jabber subscriptions jabber subscription, to: "/subs.groovy" // for Jabber user presence notifications jabber presence, to: "/presence.groovy"
Note: Those two notations are actually equivalent to:post "/_ah/mail/*", forward: "/receiveEmail.groovy" post "/_ah/xmpp/message/chat/", forward: "/receiveJabber.groovy"Should upcoming App Engine SDK versions change the URLs, you would still be able to define routes for those handlers, till a new version of Gaelyk is released with the newer paths.
Note: Make sure to read the sections on incoming email messages and incoming jabber messages.
Using wildcards
You can use a single and a double star as wildcards in your routes, similarly to the Ant globing patterns.
A single star matches at least one character up to a slash (/[^\/]+/
), where as a double start matches an arbitrary path.
To match all files by extension, use /**/*.fileext
.
For instance, if you want to show information about the blog authors,
you may forward all URLs starting with /author
to the same Groovlet:
get "/author/*", forward: "/authorsInformation.groovy"
This route would match requests made to /author/johnny
as well as to /author/begood
.
In the same vein, using the double star to forward all requests starting with /author
to the same Groovlet:
get "/author/**", forward: "/authorsInformation.groovy"
This route would match requests made to /author/johnny
, as well as /author/johnny/begood
,
or even /author/johnny/begood/and/anyone/else
.
Warning: Beware of the abuse of too many wildcards in your routes, as they may be time consuming to compute when matching a request URI to a route pattern. Better prefer several explicit routes than a too complicated single route.
Warmup requests
When an application running on a production instance receives too many incoming requests, App Engine will spawn a new server instance to serve your users. However, the new incoming requests were routed directly to the new instance, even if the application wasn't yet fully initialized for serving requests, and users would face the infamous "loading request" issue, with long response times, as the application needed to be fully initialized to be ready to serve those requests. Thanks to "warmup requests", Google App Engine does a best effort at honoring the time an application needs to be fully started, before throwing new incoming requests to that new instance.
Warmup requests are enabled by default, and new traffic should be directed to new application instances only when the following artefacts are initialized:
-
Servlets configured with
load-on-startup
and theirvoid init(ServletConfig)
method was called. -
Servlet filters have had their
void init(FilterConfig)
method called. -
Servlet context listeners have had their
void contextInitialized(ServletContextEvent)
method called.
Note: Please have a look at the documentation regarding "warmup requests". Please also note that you can also enable billing and activate an option to reserve 3 warm JVMs ready to serve your requests.
So to benefit from "warmup requests", the best approach is to follow those standard initialization procedures. However, you can also define a special Groovlet handler for those warmup requests through the URL routing mechanism. Your Groovlet will be responsible for the initialization phase your application may be needing. To define a route for the "warmup requests", you can procede as follows:
all "/_ah/warmup", forward: "/myWarmupRequestHandler.groovy"
Using path variables
Gaelyk provides a more convenient way to retrieve the various parts of a request URI, thanks to path variables.
In a blog application, you want your article to have friendly URLs.
For example, a blog post announcing the release of Groovy 1.7-RC-1 could be located at:
/article/2009/11/27/groovy-17-RC-1-released
.
And you want to be able to reuse the various elements of that URL to pass them in the query string of the Groovlet
which is responsible for displaying the article.
You can then define a route with path variables as shown in the example below:
get "/article/@year/@month/@day/@title", forward: "/article.groovy?year=@year&month=@month&day=@day&title=@title"
The path variables are of the form @something
, where something is a word (in terms of regular expressions).
Here, with our original request URI, the variables will contains the string '2009'
for the
year
variable, '11'
for month
, '27'
for day
,
and 'groovy-17-RC-1-released
for the title
variable.
And the final Groovlet URI which will get the request will be
/WEB-INF/groovy/article.groovy?year=2009&month=11&day=27&title=groovy-17-RC-1-released
,
once the path variable matching is done.
Note: If you want to have optional path variables, you can append questionmark?
to the end of the path variable.get "/article/@year?/@month?/@day?/@title?", forward: "/article.groovy"The definition above is the same like if you write following routes to display all the articles published on some year, month, or day:get "/article/@year/@month/@day/@title", forward: "/article.groovy?year=@year&month=@month&day=@day&title=@title" get "/article/@year/@month/@day", forward: "/article.groovy?year=@year&month=@month&day=@day" get "/article/@year/@month", forward: "/article.groovy?year=@year&month=@month" get "/article/@year", forward: "/article.groovy?year=@year" get "/article", forward: "/article.groovy"If you want to create sticky optional variable at the end of the route, use distinguished prefix such aspage-
in following example.get "/article/@category?/@subcategory?/page-@page?", forward: "/article.groovy"This will produce following routes:get "/article/@category/@subcategory/page-@page", forward: "/article.groovy?page=@page&subcategory=@subcategory&category=@category" get "/article/@category/page-@page", forward: "/article.groovy?page=@page&category=@category" get "/article/page-@page", forward: "/article.groovy?page=@page" get "/article/@category/@subcategory", forward: "/article.groovy?subcategory=@subcategory&category=@category" get "/article/@category", forward: "/article.groovy?category=@category" get "/article", forward: "/article.groovy"Optinonal parameters currently only works for destinations defined as String.
Also, note that routes are matched in order of appearance.
So if you have several routes which map an incoming request URI, the first one encountered in the route definition file will win.
This can be changed by passing index
parameter to the route definition. Index parameter lesser than zero will be matched
before all the other routes defined in the routes script. Be sure the index is unique. Do not assing index between zero and number of your
routes in the routes file
Validating path variables
The routing system also allows you to validate path variables thanks to the usage of a closure. So if you use path variable validation, a request URI will match a route if the route path matches, but also if the closure returns a boolean, or a value which is coercible to a boolean through to the usual Groovy Truth rules. Still using our article route, we would like the year to be 4 digits, the month and day 2 digits, and impose no particular constraints on the title path variable, we could define our route as follows:
get "/article/@year/@month/@day/@title", forward: "/article.groovy?year=@year&month=@month&day=@day&title=@title", validate: { year ==~ /\d{4}/ && month ==~ /\d{2}/ && day ==~ /\d{2}/ }
Note: Just as the path variables found in the request URI are replaced in the rewritten URL,
the path variables are also available inside the body of the closure,
so you can apply your validation logic.
Here in our closure, we used Groovy's regular expression matching support,
but you can use boolean logic that you want, like year.isNumber()
, etc.
In addition to the path variables, you also have access to the request
as well as all GAE services from within the validation closure.
For example, if you wanted to check that a particular attribute is present in the request,
like checking a user is registered to access a message board, you could do:
get "/message-board", forward: "/msgBoard.groovy", validate: { request.registered == true }
Another example would be to verify if the current user is an admin before allowing the route to kick in. The GAE services are available under the same variable names as in groovlets.
get "/only-admin", forward: "/secured.groovy", validate: { users.isUserAdmin() }
Capability-aware routing
With Google App Engine's capability service, it is possible to programmatically decide what your application is supposed to be doing when certain services aren't functionning as they should be or are scheduled for maintenance. For instance, you can react upon the unavailability of the datastore, etc. With this mechanism available, it is also possible to customize your routes to cope with the various statuses of the available App Engine services.
Note: Please make sure to have a look at the capabilities support provided by Gaelyk.
To leverage this mechanism, instead of using a simple string representing the redirect or forward destination of a route, you can also use a closure with sub-rules defining the routing, depending on the status of the services:
import static com.google.appengine.api.capabilities.Capability.* import static com.google.appengine.api.capabilities.CapabilityStatus.* get "/update", forward: { to "/update.groovy" to("/maintenance.gtpl").on(DATASTORE) .not(ENABLED) to("/readonly.gtpl") .on(DATASTORE_WRITE).not(ENABLED) }
In the example above, we're passing a closure to the forward parameter.
There is a mandatory default destination defined: /update.groovy
,
that is chosen if no capability-aware sub-rule matches.
Important: The sub-rules are checked in the order they are defined: so the first one matching will be applied. If none matches, the default destination will be used.
The sub-rules are represented in the form of chained method calls:
- A destination is defined with the
to("/maintenance.gtpl")
method. - Then, an
on(DATASTORE)
method tells which capability should the rule be checked against. -
Eventually, the
not(ENABLED)
method is used to check if theDATASTORE
isnot
in the statusENABLED
.
Tip: If you're using Groovy 1.8-beta-2 and beyond, you'll be able to use an even nicer syntax, with fewer punctuation marks:import static com.google.appengine.api.capabilities.Capability.* import static com.google.appengine.api.capabilities.CapabilityStatus.* get "/update", forward: { to "/update.groovy" to "/maintenance.gtpl" on DATASTORE not ENABLED to "/readonly.gtpl" on DATASTORE_WRITE not ENABLED }
The last method of the chain can be either not()
, as in our previous examples, or is()
.
For example, you can define a sub-rule for the case where a scheduled maintenance window is planned:
// using Groovy-1.8-beta-2+ syntax: to "/urlFetchMaintenance.gtpl" on URL_FETCH is SCHEDULED_MAINTENANCE
The following capabilities are available, as defined as constants in the Capability class:
- BLOBSTORE
- DATASTORE
- DATASTORE_WRITE
- IMAGES
- MEMCACHE
- TASKQUEUE
- URL_FETCH
- XMPP
The available status capabilities, as defined on the CapabilityStatus enum, are as follows:
- ENABLED
- DISABLED
- SCHEDULED_MAINTENANCE
- UNKNOWN
Ignoring certain routes
As a fast path to bypass certain URL patterns, you can use the ignore: true
parameter in your route definition:
all "/_ah/**", ignore: true
Caching groovlet and template output
Gaelyk provides support for caching groovlet and template output,
and this be defined through the URL routing system.
This caching capability obviously leverages the Memcache service of Google App Engine.
In the definition of your routes, you simply have to add a new named parameter: cache
,
indicating the number of seconds, minutes or hours you want the page to be cached.
Here are a few examples:
get "/news", forward: "/new.groovy", cache: 10.minutes get "/tickers", forward: "/tickers.groovy", cache: 1.second get "/download", forward: "/download.gtpl", cache: 2.hours
The duration can be any number (an int) of second(s), minute(s) or hour(s): both plural and singular forms are supported.
Note: byte arrays (the content to be cached) and strings (the URI, the content-type and last modified information) are stored in Memcache, and as they are simple types, they should even survive Google App Engine loading requests.
It is possible to clear the cache for a given URI if you want to provide a fresher page to your users:
memcache.clearCacheForUri('/breaking-news')
Note: There are as many cache entries as URIs with query strings. So if you have/breaking-news
and/breaking-news?category=politics
, you will have to clear the cache for both, as Gaelyk doesn't track all the query parameters.
Namespace scoped routes
Another feature of the URL routing system, with the combination of Google App Engine's namespace handling support, is the ability to define a namespace, for a given route. This mechanism is particularly useful when you want to segregate data for a user, a customer, a company, etc., i.e. as soon as you're looking for making your application multitenant. Let's see this in action with an example:
post "/customer/@cust/update", forward: "/customerUpdate.groovy?cust=@cust", namespace: { "namespace-$cust" }
For the route above, we want to use a namespace per customer.
The namespace
closure will be called for each request to that route,
returning the name of the namespace to use, in the scope of that request.
If the incoming URI is /customer/acme/update
, the resulting namespace used for that request
will be namespace-acme
.
Note: Make sure to have a look at the namespace support also built-in Gaelyk.
Routes indexes
Especially for writing plugins you may want to change the default behaviour where the routes are evaluated one by one by its poisition
in routes.groovy
file. Under normal circumstances the index equals the line number
of the route but can change this by assinging it in route definition:
get "/home", forward: "/home.groovy", index: -1
Also for plugins you can assign the first route index by calling startRoutingAt
method so default indexes won't start at zero but the given number:
startRoutingAt -1000 // this route will have index of -1000 get "/home", forward: "/home.groovy"