Monday, March 11, 2013

Easy extensionless URLs in JSF with OmniFaces 1.4

For some time now there's a trend going on to simplify URLs used on the web. Increasingly the trend is to remove technical clutter from URLs and make them cleaner and friendlier for humans to remember. E.g. from something like

we went to:

The protocol, "http://", is mostly skipped when printing URLs for humans to type over nowadays since browsers simply default to it. The elaborate subdomains, specifically the "www1", "www2", "wwwN" nonsense, is mostly handled internally by load balancers these days, etc.

E.g. I photographed the following from an ad that was displayed on Rembrandtplein, Amsterdam:

The extension

One specific technical part of the URL that can also be shaved off is the extension (".jspx" in the example above), and this will be the topic of this article.

Technically, an extension can have two uses:

It can simply be a part of the file name, but with a specific meaning. E.g. "foo.jpg" and "foo.html" are clearly two distinct files, but because of their extension an OS or a web server knows what the type of the file is. In case of the OS it can be used to select the application that will open the file, while the web server can use it to base the response headers on. In this last case, the browser doesn't need to see the extension to determine what the content type is, since it will be in the headers.

Secondly, an extension can be used in a request to tell the web server which of its internal "handlers" should process the request. E.g. "foo.jpg" and "foo.html" may be directly streamed from disk, while "foo.jsp" will be handled by a JSP engine (Servlet Container) and "foo.php" by a PHP interpreter, etc.

In the case of JSF, both usages come into play. By default the so-called Faces Servlet (responsible for handling JSF requests) is mapped to the extensions ".jsf" and ".faces", while the file on disk that contains the actual view content corresponding to the request is by default either ".xhtml" (Facelets), or ".jsp" (JSP, deprecated for JSF). A popular user-defined mapping is to use ".xhtml" for both the Servlet Mapping and the file on disk.

So if no extensions are used in the URL, how does the web server knows which "handler" to invoke for a given request?

The answers is that without additional measures it just doesn't know this. There are a couple of solutions to this dilemma though.

The first is to have a filter inspect every request and forward every one without an extension to the Faces Servlet. Because of the fact that JSF inspects the URL in order to derive the physical file location from it, we have to attach an extension before forwarding anyway, but this is mostly a technicality and does not change the general principal.

Although a forward-all filter does do the trick, it might be too much of a blunt instrument.

Another typical solution is to have some sort of configuration file where one can specify per (extensionless) URL to which resource should be forwarded. This allows for an unprecedented amount of control and even opens options for elaborate mapping of path segments to request parameters. The downside however is that having to put each and every URL in such file is a lot of overhead and there's a maintenance cost involved in keeping file names and resource forwards in sync.

As a middle ground between those two approaches, OmniFaces has introduced another approach dubbed FacesViews; upfront scanning of files based on a "well known" folder or one or more explicitly specified folders. No- or little configuration has been an important design consideration for this approach.

OmniFaces file location variations

There are three variations on the approach mentioned in the previous section, which are discussed below:

1. Put files in a special dedicated folder.

OmniFaces recognizes a special folder: WEB-INF/faces-views. All files put in there are automatically scanned and made available as JSF views, both via their original extension as well as without an extension. The folder functions as a root path, meaning that e.g. WEB-INF/faces-views/foo.xhtml will be mapped to the URL /foo.

Using this method no configuration whatsoever is needed. It even works without any web.xml or faces-config.xml present, and it thus also doesn't need the Faces Servlet to be mapped to any URL pattern.

Because all files are scanned, only files that are actually JSF views should be put in there though.

One disadvantage of this method is that IDEs will not expect JSF views to be stored in this location and thus might not work properly. E.g. navigation in the editor to other views by clicking on links or includes might not work. Users who don't know OmniFaces might also not expect to find the JSF views inside WEB-INF.

2. Put files in special pre-configured folders

Another option is to configure one or more folders of one's own choice that will then be scanned by OmniFaces. Such folders can reside either directly in the web root, or in WEB-INF as well. If a folder in the web root is used, any files put there will also be available via the path starting at the web root.

Using this method it's possible to make logical groups of views without this group name being exposed in the URL. It's mostly for advanced cases though, i.e. the OmniFaces showcase app uses this to distinguish views that show examples (and thus are scanned for their own source code), from other views.

3. Scan the root folder for specific file extensions

A particular convenient option is to use the second method to configure a custom folder, and then use the root folder ("/") for this. Of course we normally don't want all files that we have there, like .css, .js, etc, mapped to the Faces Servlet. To prevent this we can specify that only a certain file extension should be scanned by adding a wild card to the folder (eg "/*.xhtml").

The big advantage of this method is that all JSF view files are at their expected location, which means tools will work as they normally do. The price is that a small amount of configuration is needed.
The following shows an example of this:

Forwarding vs Request modification

A typical approach to implement extensionless URLs, whether automatic scanning or explicit mapping files are used or not, is by installing a Servlet Filter that forwards to the Faces Servlet.

While forwards do work, interaction with other filters might be problematic. Having the forward filter as the first one in a filter chain will totally exclude the next filters from being invoked, which is surely not wanted. Having the forward filter as the last one however is also not optimal as filters before it will be invoked, but are not allowed to modify the response.

To fix this, some or all Filters may have to be set to dispatch type FORWARD. While this typically works, there might be issues with this approach when the same filters are also needed for non-forwarded resources and it's not trivial to keep their URL patterns apart.

While OmniFaces has a forward option as well, its default is to not forward but instead wrap the request and simply continue the filter chain. This trick does requires the Faces Servlet to be mapped on the original extensionless request. E.g. for a request like localhost:8080/foo the Faces Servlet needs to be mapped to /foo.

Clearly, this is a less than ideal approach when all those exact mappings (=mapping without wildcards) have to be added manually. Luckily though, Servlet 3.0 introduced API for programmatically adding mappings to existing Servlets. Since OmniFaces already has the full collection of scanned views to its disposal, adding those mappings is relatively simple. Needless to say, this method doesn't work very well with Servlet 2.5 containers. For this reason, and for any unforeseen problems with the request modification approach, OmniFaces' FacesViews also provides the forward method.

Demo and code

OmniFaces' own showcase app extensively uses FacesViews. It can be found at:

OmniFaces 1.4.1 can be easily added to a Maven project via the following coordinates in pom.xml:
Alternatively the OmniFaces 1.4.1 jar file can be downloaded directly and put into WEB-INF/lib.

Extra documentation can be found at the Javadocs for the FacesViews package.

Arjan Tijms