Matt RaibleMatt Raible is a Web Architecture Consultant specializing in open source frameworks.

10+ YEARS


Over 10 years ago, I wrote my first blog post. Since then, I've authored books, had kids, traveled the world, found Trish and blogged about it all.

Implementing Extensionless URLs with Tapestry, Spring MVC, Struts 2 and JSF

For the past couple of weeks, I've spent several evening hours implementing extensionless URLs in AppFuse. I've been wanting to do this ever since I wrote about how to do it a few years ago. This article details my experience and will hopefully help others implement this feature in their webapps.

First of all, I used the UrlRewriteFilter, one of my favorite Java open source projects. Then I followed a pattern I found in Spring's "mvc-basic" sample app from MVC Simplifications in Spring 3.0. The app has since changed (because SpringSource integrated UrlRewriteFilter-type functionality in Spring MVC), but the pattern was basically path-matching instead of extension-mapping. That is, the "dispatcher" for the web framework was mapped to /app/* instead of *.html.

Prior to the move to extensionless URLs, AppFuse used *.html for its mapping and this seemed to cause users problems when they wanted to serve up static HTML files. To begin with, I removed all extensions from URLs in tests (Canoo WebTest is used for testing the UI). I also did this for any links in the view pages and redirects in the Java code. This provided a decent foundation to verify my changes worked. Below are details about each framework I did this for, starting with the one that was easiest and moving to hardest.

Tapestry 5
Tapestry was by far the easiest to integrate extensionless URLs into. This is because it's a native feature of the framework and was already integrated as part of Serge Eby's Tapestry 5 implementation. In the end, the only things I had to do where 1) add a couple entries for CXF (mapped to /services/*) and DWR (/dwr/*) to my urlrewrite.xml and 2) change the UrlRewriteFilter so it was only mapped to REQUEST instead of both REQUEST and FORWARD. Below are the mappings I added for CXF and DWR.

<urlrewrite default-match-type="wildcard">
    ...
    <rule>
        <from>/dwr/**</from>
        <to>/dwr/$1</to>
    </rule>
    <rule>
        <from>/services/**</from>
        <to>/services/$1</to>
    </rule>
</urlrewrite>

Spring MVC
I had a fair amount of experience with Spring MVC and extensionless URLs. Both the Spring MVC applications we developed last year at Time Warner Cable used them. To change from a *.html mapping to /app/* was pretty easy and involved removing more code than I added. Previously, I had a StaticFilter that looked for HTML files and if it didn't find them, it dispatched to Spring's DispatcherServlet. I was able to remove this class and make the web.xml file quite a bit cleaner.

To make UrlRewriteFilter and Spring Security play well together, I had to move the securityFilter so it came after the rewriteFilter and add an INCLUDE dispatcher so included JSPs would have a security context available to them.

<filter-mapping>
    <filter-name>rewriteFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
    <filter-name>securityFilter</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>FORWARD</dispatcher>
    <dispatcher>INCLUDE</dispatcher>
</filter-mapping>

The only other things I had to change were security.xml and dispatcher-servlet.xml to remove the .html extensions. The urlrewrite.xml file was fairly straightforward. I used the following at the bottom as a catch-all for dispatching to Spring MVC.

<rule>
    <from>/**</from>
    <to>/app/$1</to>
</rule>
<outbound-rule>
    <from>/app/**</from>
    <to>/$1</to>
</outbound-rule>

Then I added a number of other rules for j_security_check, DWR, CXF and static assets (/images, /scripts, /styles, /favicon.ico). You can view the current urlrewrite.xml in FishEye. The only major issue I ran into was that Spring Security recorded protected URLs as /app/URL so I had to add a rule to redirect when this happened after logging in.

<rule>
    <from>/app/**</from>
    <to last="true" type="redirect">%{context-path}/$1</to>
</rule>

Struts 2
Using extensionless URLs with Struts 2 is likely pretty easy thanks to the Convention Plugin. Even though this plugin is included in AppFuse, it's not configured with the proper constants and I have struts.convention.action.disableScanning=true in struts.xml. It looks like I had to do this when I upgraded from Struts 2.0.x to Struts 2.1.6. It's true AppFuse's Struts 2 support could use a bit of love to be aligned with Struts 2's recommended practices, but I didn't want to spend the time doing it as part of this exercise.

With Struts 2, I tried the path-mapping like I did with Spring MVC, but ran into issues. Instead, I opted to use an ".action" extension by changing struts.action.extension from "html" to "action," in struts.xml. Then I had to do a bunch of filter re-ordering and dispatcher changes. Before, with a .html extension, I had all filters mapped to /* and in the following order.

Filter NameDispatchers
securityFilter request
rewriteFilter request, forward
struts-prepare request
sitemesh request, forward, include
staticFilter request, forward
struts request

Similar to Spring MVC, I had to remove the rewriteFilter in front of the securityFilter and I was able to remove the staticFilter. I also had to map the struts filter to *.action instead of /* to stop Struts from trying to catch static asset and DWR/CXF requests. Below is the order of filters and their dispatchers that seems to work best.

Filter NameDispatchers
rewriteFilter request
securityFilter request, forward, include
struts-prepare request, forward
sitemesh request, forward, include
struts forward

From there, it was a matter of modifying urlrewrite.xml to have the following catch-all and rules for static assets, j_security_check and DWR/CXF.

<rule match-type="regex">
    <from>^([^?]*)/([^?/\.]+)(\?.*)?$</from>
    <to last="true">$1/$2.action$3</to>
</rule>
<outbound-rule match-type="regex">
    <from>^(.*)\.action(\?.*)?$</from>
    <to last="false">$1$2</to>
</outbound-rule>

JSF
JSF was by far the most difficult to get extensionless URLs working with. I'm not convinced it's impossible, but I spent a several hours over a few days and was unsuccessful in completely removing them. I was able to make things work so I could request pages without an extension, but found when clicking buttons and links, the extension would often show up in the URL. I'm also still using JSF 1.2, so it's possible that upgrading to 2.0 would solve many of the issues I encountered.

For the time being, I've changed my FacesServlet mapping from *.html to *.jsf. As with Struts, I had issues when I tried to map it to /app/*. Other changes include changing the order of dispatchers and filters, the good ol' catch-all in urlrewrite.xml and modifying security.xml. For some reason, I wasn't able to get file upload working without adding an exception to the outbound-rule.

<rule match-type="regex">
    <from>^([^?]*)/([^?/\.]+)(\?.*)?$</from>
    <to last="true">$1/$2.jsf</to>
</rule>
<outbound-rule match-type="regex">
  <!-- TODO: Figure out how to make file upload work w/o using *.jsf -->
    <condition type="path-info">selectFile</condition>
    <from>^(.*)\.jsf(\?.*)?$</from>
    <to last="false">$1$2</to>
</outbound-rule>

I also spent a couple hours trying to get Pretty Faces to work. I wrote about my issues on the forums. I tried writing a custom Processor to strip the extension, but found that I'd get into an infinite loop where the processor kept getting called. To workaround this, I tried using Spring's RequestContextHolder to ensure the processor only got invoked once, but that proved fruitless. Finally, I tried inbound and outbound custom processors, but failed to get those working. The final thing I tried was url-mappings for each page in pretty-config.xml.

<url-mapping>
  <pattern value="/admin/users"/>
  <view-id value="/admin/users.jsf"/>
</url-mapping>
<url-mapping>
  <pattern value="/mainMenu"/>
  <view-id value="/mainMenu.jsf"/>
</url-mapping>

The issue with doing this was that some of the navigation rules in my faces-config.xml stopped working. I didn't spend much time trying to diagnose the problem because I didn't like having to add an entry for each page in the application. The one nice thing about Pretty Faces is it did allow me to do things like the following, which I formerly did with a form that auto-submitted when the page loaded.

<url-mapping>
  <pattern value="/passwordHint/#{username}"/>
  <view-id value="/passwordHint.jsf"/>
  <action>#{passwordHint.execute}</action>
</url-mapping>

Conclusion
My journey implementing extensionless URLs was an interesting one, and I solidified my knowledge about ordering of filters, dispatchers and the UrlRewriteFilter. I still think I have more to learn about properly implementing extensionless URLs in Struts 2 and JSF and I hope to do that in the near future. I believe Struts' Convention Plugin will help me and JSF 2 + Pretty Faces will hopefully work nicely too. Of course, it'd be great if all Java Web Frameworks had an easy mechanism for producing and consuming extensionless URLs. In the meantime, thank goodness for the UrlRewriteFilter.

If you'd like to try AppFuse and its shiny new URLs, see the QuickStart Guide and choose the 2.1.0-SNAPSHOT version.

Posted in Java at Feb 10 2011, 04:53:27 PM MST 10 Comments
Comments:

Hello Matt,

interesting article, as usual :)
I'm tryng to develop extensionless all my last projects using SpringMVC + UrlRewrite.

I've noted a difference in the filters order inside web.xml. I'm using springsecurity as first filter as they say in reference:

7.5 Use with other Filter-Based Frameworks

If you're using some other framework that is also filter-based, then you need to make sure that the Spring Security filters come first. This enables the SecurityContextHolder to be populated in time for use by the other filters. Examples are the use of SiteMesh to decorate your web pages or a web framework like Wicket which uses a filter to handle its requests.


why did you invert this order? which problems have you encountered?

Posted by emilime on February 10, 2011 at 06:36 PM MST #

Hey Matt,

Nice post! To elaborate a bit on the Spring MVC sample project change you mentioned: yes, previously we integrated URLRewriteFilter to get "clean URLs" with path-based servlet mappings such as /app/*, exactly as you described. Then we realized UrlRewriteFilter wasn't required to get clean URLs after all, as you could simply map the Spring MVC DispatcherServlet to the "default" servlet-mapping of "/" (instead of "/*").

I have found this "/" servlet-mapping pattern to work well overall, and it is now used in mvc-basic, the MVC project templates in STS and Roo, and more realistic reference apps such as Greenhouse.

The one consequence of using this pattern is it does require the "/"-mapped default servlet to care for static resource requests, unless a more specific ResourceServlet is registered (say with a servlet-mapping of /resources), or you have those served up from a separate domain. To address this, we added a mvc:resources handler in Spring MVC 3.0.4, allowing the DispatcherServlet to handle static resource requests, too.

I'm curious what you and others think about this approach vs. integrating URLRewriteFilter for clean URLs. Our main motivation for this was to have one less dependency for the clean URL case.

Cheers,
Keith

Posted by Keith Donald on February 10, 2011 at 08:33 PM MST #

Hi Matt,

thank you for sharing your experience.

In Struts2 you can also use the rest plugin for extensionless URLs without the UrlRewriteFilter.

Regards

Johannes

Posted by Johannes Geppert on February 11, 2011 at 02:50 AM MST #

@emilime - If you look at the web.xml for Spring MVC, you'll see that I have the following filters before the Spring Security Filter: sitemesh, encodingFilter, localeFilter and rewriteFilter. The reason I'm able to put SiteMesh as #1 is because 1) I don't have anything in my decorator that relies on the security context and 2) any JSPs that do are included, which means they're filtered by the INCLUDE dispatcher of the securityFilter. I have the rewriteFilter before the securityFilter because it's responsible for changing URLs and forwarding them to secure URLs. If I had the securityFilter after the rewriteFilter, it wouldn't see those protected URLs.

@Keith - For the mvc:resources handler to work with static assets, do you need an entry for /images, /styles, /scripts, etc? What about ancillary services like CXF and DWR? Do their servlet-mappings allow them to have priority for their URLs?

I still think UrlRewriteFilter might be necessary for proxying cross-domain Ajax, but I do like the idea of a web framework doing clean URLs out-of-the-box.

Posted by Matt Raible on February 11, 2011 at 09:07 AM MST #

Matt,

Yes, you can add multiple resource locations:

<mvc:resources mapping="/resources/**" location="/images/, /styles/, /scripts/" />

These days I tend to prefer organizing within a single root resources directory:

<mvc:resources mapping="/resources/**" location="/resources/" />

(images, styles, scripts, etc would live inside /resources then). You can also serve resources up from jars in the classpath.

Yes, when you use the default servlet mapping of "/", other servlets with more specific mappings e.g. "/cxf or /dwr" will be matched first.

Posted by Keith Donald on February 11, 2011 at 01:59 PM MST #

@Keith - for multiple resource locations, do you still refer to the files using /images, /scripts, etc in your view pages? I agree about the single resources directory. I like to call mine "assets".

What about versioning static assets - are you able to do that as a native feature of Spring MVC?

Posted by Matt Raible on February 11, 2011 at 03:39 PM MST #

Matt,
Good question. Yeah, for each location, you would refer to what's contained within, and would not include the location names themselves in your URLs. In the example above, if you wanted '/images/foo.jpg' style resource URLs in your views, you would need to add the containing parent directory as a resource location. This could be the webapp root ("/"), and /WEB-INF would be excluded automatically. Though, as we both agree, a common parent directory named /resources/ or /assets/ would be preferable.

Versioning is something we haven't yet introduced but is scheduled for the next Spring Framework 3.1 milestone, along with several related items. If you could summarize what you'd like to see there, that could help us focus our efforts.

Thanks!
Keith

Posted by Keith Donald on February 13, 2011 at 05:46 PM MST #

Matt,

Though we do indeed intend to consider more explicit support for versioning of assets in 3.1, I just wanted to point out that you can still achieve this with the current mvc:resources tag by making use of SpEL. There's actually an example of this included in the docs. That particular example shows versioning at a fairly coarse level, but you can get more specific for various resources (say you wanted to use the actual version number of a given JS library) by splitting things out into multiple mvc:resources tags.

Also worth noting is the mvc:default-servlet-handler tag, which is needed for any static resources *not* being handled by mvc:resources when you're mapping the DispatcherServlet to "/". Whereas mvc:resources actually handles the serving of the resources directly and allows you more fine-grained control over things such as setting cache headers, mvc:default-servlet-handler actually forwards requests to the Servlet container's default servlet.

Cheers,
Jeremy

Posted by Jeremy Grelle on February 13, 2011 at 07:06 PM MST #

@Matt and @Keith

What about URL routing "a la" Rails/Play!/Symfony?

I've tried to rollout my own on bitbucket - so far it's working well.

Posted by Brian on March 03, 2011 at 11:23 AM MST #

There is a new (soon to be released) Tynamo module for Tapestry5 that does URL routing "a la" RoR/Play!/Sitebricks. It's called tapestry-routing, check it out: tapestry-routing guide

Posted by ascandroli on March 23, 2011 at 08:09 AM MDT #

Post a Comment:
  • HTML Syntax: Allowed