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.

Extensionless URLs with Java Web Frameworks

Last week, I had a go of making a Spring MVC application use extensionless URLs. I did some googling, found some tips on the Spring Forums and believe I arrived at a solid solution. Using the UrlRewriteFilter (version 3), I was able to create a rule that looks for any URLs without an extension. If it finds one, it appends the extension and forwards to the controllers. This rule is as follows (where *.html is my servlet-mapping for DispatcherServlet in web.xml):

  <rule>
    <from>^([^?]*)/([^?/\.]+)(\?.*)?$</from>
    <to last="true">$1/$2.html$3</to>
  </rule>

As long as I hand-write all my URLs without an extension (<a href="home"> vs. <a href="home.html">), this seems to work. To combat developers that use "home.html", one solution is to require all links to be wrapped with <c:url value="url"/> (or some other macro that call response.encodeURL()). If you can convince everyone to do this, you can write an outbound-rule that strips the .html extension from URLs.

  <outbound-rule>
    <from>^(.*)\.html(\?.*)?$</from>
    <to last="false">$1$2</to>
  </outbound-rule>

In an ideal world, it'd be possible to modify the <a> tag at the very core of the view framework you're using to automatically encode the URL of any "href" attributes. I don't think this is possible with JSP, FreeMarker, Facelets or any other Java Web Framework templates (i.e. Tapestry or Wicket). If it is, please let me know.

Below is my final urlrewrite.xml with these rules, as well as my "welcome-file" rule at the top.

<?xml version="1.0" encoding="utf-8"?>
<!DOCENGINE urlrewrite PUBLIC "-//tuckey.org//DTD UrlRewrite 3.0//EN"
  "http://tuckey.org/res/dtds/urlrewrite3.0.dtd">

<urlrewrite>
  <rule>
    <from>/$</from>
    <to type="forward">home</to>
  </rule>

  <rule>
    <from>^([^?]*)/([^?/\.]+)(\?.*)?$</from>
    <to last="true">$1/$2.html$3</to>
  </rule>

  <outbound-rule>
    <from>^(.*)\.html(\?.*)?$</from>
    <to last="false">$1$2</to>
  </outbound-rule>

</urlrewrite>

If you have other solutions for extensionless URLs with Java web frameworks, I'd love to hear about them. With any luck, 2008 will be the year we drop extensions (and path-mappings) from our URLs. The stat packages might not like it, but I do.

Posted in Java at May 13 2008, 09:50:51 PM MDT 18 Comments
Comments:

Hi Matt,
I created a Rails-like (and therefore Grails-like) extension to Spring MVC that I called Agile MVC. I haven't promoted it as an Open Source project, but I have posted the source to Google Code (since I have a client that is using it.)

(I mentioned this to you in a bar once...)

It uses a Spring MVC HandlerMapping to do use Rails-style "routes" to map from a path to a controller. It can even parse parameters out of URL components as part of the mapping process. Grails does many of the same things, but the idea here was to create something that could be used in regular Java.

Configuration is a little tedious as it uses simple Java beans wired up with basic Spring Bean XML. A Spring XML vocabulary or a Groovy Builder might be nicer, but what is there will work. There is a small amount of (un-rendered) DocBook documentation, but Google code doesn't let you post static HTML and I haven't learned how to use any of the Maven 2 DocBook plugins.

I'd be willing to contribute this to AppFuse if you were interested. I might even be able to be cajoled into working on it and/or documenting it a little more. I'd love any feedback from you.

Posted by Sean Gilligan on May 13, 2008 at 11:04 PM MDT #

Thanks Matt, I've been wanting to do something like this for a while. I also frequently struggle with the "best" way to manage URLs and the servlet context part of the path when I want my urls to hang off the root (e.g. http://www.foo.com/someform.html as opposed to http://www.foo.com/app/someform.html). Right now I'm cheating by making my webapp ROOT (using Context path=""). I don't think that's going to work if I want to host multiple apps in a single tomcat fronted (via mod_jk) by a single Apache, without some sort of rewrite hacking.

One thing that had bothered me about the UrlRewriteFilter approach was that for some odd reason I had initially thought that the outbound-rule stuff was processed by parsing the entire output (which would be somewhat expensive). But looking at the docs, it looks like that assumption was incorrect.

It's a shame that some of this stuff isn't easier. I'll have to look into Sean's approach (previous comment). It does seem to me that Spring MVC should have a better way of handling and "mounting" URLs (as I think they call it in Wicket).

Posted by Mark Helmstetter on May 13, 2008 at 11:39 PM MDT #

The webwork url tag also works well with the UrlRewriteFilter, although it tends to lose the order of parameters, causing the outbound rule to fail occasionally - although we managed to fix this. Found this to be a better solution than apache mod-rewrite and hundreds of IF conditions to test whether URL's are to be re-written or not. By using UrlRewriteFilter and the ww:url tag we were able to easily switch url rewrites on or off easily, and without conditional statements throughout the code.

Posted by Jason on May 14, 2008 at 07:06 AM MDT #

I just use my own craptaculous framework (Feather) that is based entirely on filters and maps URLs to class methods based upon regular expressions.

For the life of me, I could never understand why Servlet mappings had to be done using only extensions.

Posted by Marcus Breese on May 14, 2008 at 09:34 AM MDT #

URL rewriting is actually a default feature even inside Tapestry 4. All links rendered by Tapestry are services links, e.g. page service renders a page, asset service renders an asset, etc.

You can simply overwrite the IEngineService responsible for the page service link creation and you have it, you can write your link the way you want. And the most interesting part that links will be modified as on rendering side also on processing, so rendered links will be consistently processed.

P.S. Check the new article about T5, I think it's time to start considering it for AppFuse - http://www.infoq.com/articles/tapestry5-intro

Posted by Renat Zubairov on May 14, 2008 at 12:29 PM MDT #

Seems a bit more complicated than

<code> "/$controller/$action?/$id?"() </code> In Grails url mappings ;-)

Posted by Graeme Rocher on May 14, 2008 at 01:23 PM MDT #

Graeme - since you're using Spring MVC under-the-covers in Grails, how have you accomplished this? Are you using a Grails-specific HandlerMapping or a filter perhaps?

Posted by Matt Raible on May 14, 2008 at 01:24 PM MDT #

I recall seeing a HandlerMapping when I looked at the Grails source a while back...

Posted by Sean Gilligan on May 14, 2008 at 01:43 PM MDT #

To Mark re: multiple root contexts... If you are willing to drop Apache, you can accomplish this with Tomcat virtual hosting (and the necessary DNS entries for your domain names). The easiest way is to make copies of your webapps directory ie: fooapps and barapps, And then in server.xml, replace this:

<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true" xmlValidation="false" xmlNamespaceAware="false"> </Host>

With something like:

<Host name="foo.com" appBase="fooapps" unpackWARs="true" autoDeploy="true" xmlValidation="false" xmlNamespaceAware="false"> <Alias>www.foo.com</Alias> </Host>

<Host name="bar.com" appBase="barapps" unpackWARs="true" autoDeploy="true" xmlValidation="false" xmlNamespaceAware="false"> <Alias>www.bar.com</Alias> </Host>

Also edit this line to reflect a valid default:

<Engine name="Catalina" defaultHost="foo.com">

Now, if someone would point me in the direction of subdomain support in Appfuse-Struts 2 where URLs bob.foo.com and tom.foo.com would resolve within the foo webapp to bobindex.jsp and tomindex.jsp respectively, I would much appreciate it.

I would also be interested in the Grails solution. That could swing me.

-Bron

Posted by Bron on May 15, 2008 at 09:38 AM MDT #

As Renat points out, "extensionless" is possible in T4 and automatic in T5. The URLs are short, and human-readable (i.e., "/user/edit/23" for the EditUser page and user id 23). Let's raise the bar higher: URLs in T5 (at least the portion that identifies folders and page names and component ids) are case insensitive. That makes it easier to link to T5 apps externally, or in a mixed framework application (such as Struts + Tapestry). Or when you hand hack the URL.

Posted by Howard Lewis Ship on May 15, 2008 at 11:00 AM MDT #

Hi Matt,

you could also map the Spring DispatcherServlet to "/*". Obviously this would would overwrite the default servlet but you could either use Apache to serve the static files, or (useful during development) use a Spring HandlerMapping that serves the static resources, for example http://www.riotfamily.org/api/latest/index.html?org/riotfamily/common/web/mapping/ResourceHandlerMapping.html

for Grails-like parameter extraction you can use the AdvancedBeanNameHandlerMapping provided by Riot: http://www.riotfamily.org/api/latest/index.html?org/riotfamily/common/web/mapping/AdvancedBeanNameHandlerMapping.html

Note that this HandlerMapping also allows you to perform reverse look-ups. You can for example write something like ${common.urlForHandler('someController')} in your FreeMarker views.

Posted by Felix Gnass on May 15, 2008 at 11:58 AM MDT #

Finally, for T4, don't forget the UriTemplate annotation for pages from http://tacos.sf.net @UriTemplate("/search.html") or @UriTemplate("/my/search"), e.t.c.

Posted by Andreas Andreou on May 15, 2008 at 04:06 PM MDT #

Speaking of Web frameworks, I really value the perspectives of the people who read this blog (and obviously the individual who writes it), so if you have any opinions on my business unit's quest to standardize on either Spring MVC or JSF (plus other libraries) as the Java-based Web application framework going forward, I would love to hear your opinions:

http://theosophe74.blogspot.com/2008/05/spring-mvc-or-jsf.html

Thanks,
Mike

Posted by Michael S on May 15, 2008 at 07:00 PM MDT #

Struts2, 2.1.x allows extensionless actions. Check out: https://issues.apache.org/struts/browse/WW-2163 and http://www.nabble.com/-s2--The-death-of-the-.action-extension-td12567427.html#a12575191 Works very nicely, although in my initial tests <s:url .../> appends .action to the urls it generates. I am sure there is some configuration I need to do for these "outbound routes generation" to make them extensionless as well.

Posted by Dusty Pearce on May 16, 2008 at 09:30 AM MDT #

[Trackback] I had a brief encounter with aspects at the Colorado Software Summit a couple years back. I was glad to finally get a bit oriented to AOP, to which I had been blissfully ignorant, and decided that I had incredibly limited interest in the topic. Of al...

Posted by Scott Mark on May 29, 2008 at 08:02 PM MDT #

thanks for this great tip ! in order to work with urlrewriter filter with struts2, do not forget to add "<dispatcher>FORWARD</dispatcher><dispatcher>REQUEST</dispatcher>" to the web.xml's filter-mapping section..

Posted by Gokhan Demir on July 09, 2008 at 05:07 AM MDT #

Thanks for this tip Matt! I was looking for some sort of solution to this problem and this works great.

There is one issue. If you are using DWR, the DWR Test interface is broken. I found adding the following condition statement to my rule and outbound-rule fixes DWR:

<condition type="request-uri" operator="notequal">^/dwr/.*</condition>

Posted by Mike Wille on July 14, 2008 at 12:33 PM MDT #

Extensionless URLs are easy using Wicket (just mount it, and optionally use an URL Strategy).

It's also possible to get Wicket to automatically encode parameters (either by letting Wicket handle to URL generation, or using a custom written Link).

Posted by John on February 14, 2011 at 02:34 AM MST #

Post a Comment:
  • HTML Syntax: Allowed