Trifork Blog

AngularJS training

Wicket root mounts

February 24th, 2010 by
| Reply

One of the very easy things with Wicket is mounting pages on the first words of a URL; listening to URLs like http://shop.nl/article/4513 is programmed before you can say xiphophorus clemenciae.
One of the very hard things with Wicket is mounting pages where the first words of the URL are a parameter; listening to a URL like http://twitter.com/erik_van_oosten is just completely impossible.

Completely impossible? Wicket 1.5 will make this easy but is very instable for now. This article shows you how to mount pages on the root URL with Wicket 1.4. We will need to apply some hacks so hang on!

If you are only interested in using the technique you can skip to the last section 'Understanding the demo'.

Note: when all URLs in your site start with a predictable and/or common parameter (like the locale in /en/timetable/thisweek) you are better of with URL rewriting (e.g. to /timetable/thisweek?lang=en). Another solution is in Wicket's Wiki. This article focuses on URLs that start with a very flexible word (as in http://www.hotel.com/amsterdam and http://www.tipspot.com/sarah/agenda).

Terminology

Before we dive in the details, we first need to get some terminology straight:

Mount path
A URL prefix for which you want to parse the URL in a certain way. For example /article is the mount path for all article pages.
URL mounting
Shorthand for mounting a URL strategy; e.g. calling WebApplication.mount in your Application.init method. The effect is that URLs with the given mount path are processed by the given target page.
Root mount
Mounting a URL strategy for some URLs, but without a mount path, and some given target page.
URL strategy
This term is my abbreviation for Wicket's IRequestTargetUrlCodingStrategy; a piece of code that can parse and encode URLs. Url strategies are normally associated with a single mount path and a target page.
Root URL strategy
My term for a URL strategy that does not need a mount path to parse and encode URLs for some target page.

Tricking Wicket - the fake mount path

By default Wicket uses a mount path to select one of the the mounted URL strategies, effectively selecting a page based on the first words of a URL. This works quite well until you want an empty mount path. So how do we get around this? The trick is to fake it! Just before the URL is decoded (parsed) by Wicket we add a 'fake' mount path. Similarly just before a Wicket encoded URL is put in a HTML page, we remove the 'fake' mount path.

The advantage of this approach is that we can reuse lots of Wicket code and not have to write our own URL decoding and encoding. We simply extend the existing implementations of IRequestTargetUrlCodingStrategy (e.g. IndexedParamUrlCodingStrategy, MixedParamUrlCodingStrategy, etc.).

So that's the basic idea. We still need to jump through some hoops to make it work.

Selecting a root URL strategy

Having a fake mount path does not give you a way to select the desired root mounted URL strategy. The fake mount path is really fake, it is only present after a URL strategy has been selected.

To enable the selection process I introduce a simple interface which our root URL strategies have to implement:

interface RootMountedUrlCodingStrategy {
  boolean accepts(String rawPath);
}

The first root URL strategy to return true will be the selected URL strategy.

We can hookup a list of root URL strategies in a custom request cycle processor. Our custom request cycle processor extends UrlCompressingWebRequestProcessor (because it gives nicer URLs for non-mounted pages) and overrides newRequestCodingStrategy to return our custom IRequestCodingStrategy which extends UrlCompressingWebCodingStrategy and overrides IRequestTargetUrlCodingStrategy urlCodingStrategyForPath(String path). For more details see the code in the demo. One thing you might notice is that the root URL strategies are only consulted when no regular Wicket mount path was detected.

Adding and removing the fake mount path

The second hoop is that we can only remove the 'fake path' in the encode method of IRequestTargetUrlCodingStrategy implementations, adding it in the decode method as well will confuse Wicket too much. To solve this, our custom IRequestCodingStrategy of the previous section also overrides String getRequestPath(Request request). This is slightly tricky as we do not want to touch URLs for regular mounted paths.

To complete the Wicket trickery, RootMountedUrlCodingStrategy will extend IRequestTargetUrlCodingStrategy (so that it becomes a full URL strategy) and it will be mounted on the fake mount path so that Wicket can find it to encode URLs.

Implementing RootMountedUrlCodingStrategy

Now we need to implement instances of RootMountedUrlCodingStrategy. In general what you want is behavior similar to one of the URL strategies that extend from BookmarkablePageRequestTargetUrlCodingStrategy (with subclasses like IndexedParamUrlCodingStrategy and MixedParamUrlCodingStrategy). Unfortunately the method we need to override, encode(IRequestTarget), is final. Our third hoop. Bummer.

Update 2010-04-12: From Wicket 1.4.8 onwards this 'final' is gone. See comments below.

When there is a strong need to override a method that Wicket made final (mostly with good reason), I usually just copy the Wicket class to my own sources and make sure it has the same package. Your own classes are sure to be earlier on the classpath and therefore take precedence over the classes from the Wicket jar. An infinitely better way would be to build your own version of Wicket (for example by cloning the repository on github). In the demo I just use the hacky way.

Understanding the demo

The demo is a Wicket quickstart. That means that it uses Maven 2. After you unzipped it you can do a mvn jetty:run to start it immediately. See the Wicket quickstart page to see how you hook it up in your IDE. Once that is done, just run the Start class to start the application in an embedded Jetty container. Browse to http://localhost:8080/ to see the root mounts in action.

In the code you'll find the following packages:

nl.jteam.example
Contains the example application. For configuration see the class WicketApplication.
nl.jteam.rootmount
Contains all the code to make root mounts possible. These can be copied to your project ad verbatim.
org.apache.wicket.request.target.coding
Monkey-patch to remove a 'final' keyword in the Wicket code base (see comment above).

If you open WicketApplication you will notice 2 root mounts and one traditional mount. The first root URL strategy listens for 'cities' with an optional subject; /amsterdam and /paris/theatre and is associated with CityPage. The second root URL strategy listens for 'members'; /alice and /bob and is associated with MemberPage. The traditional mount couples mount path tokyo to page TokyoPage. Other code wires up the custom request cycle processor.

As a bonus WicketApplication shows you how to use a 'not found' page from within Wicket (commented out). Take note however that this removes the fall-back request-handling of the servlet container. See the comments on how to enable it. You have been warned!

To keep things interesting 'paris' is both the name of a city and that of a member. The city Paris 'wins' because it is the first root URL strategy (see what happens if you change the order). Furthermore, 'Tokyo' is a recognized city. URL /tokyo will however not lead to the CityPage as traditional mounts take precedence.

Package nl.jteam.rootmount also contains a couple of base classes that take the angle out of properly implementing RootMountedUrlCodingStrategy. They are: RootMountedBookmarkablePageRequestTargetUrlCodingStrategy, RootMountedIndexedParamUrlCodingStrategy.java, RootMountedMixdedParamUrlCodingStrategy.java and RootMountedQueryStringUrlCodingStrategy.java. They correspond to the normal Wicket equivalents. All you have to do is extend them and implement boolean accepts(String rawPath). Examples are CityPageUrlCodingStrategy and MemberPageUrlCodingStrategy.

Using root mounts in your project

To apply this technique in your own application, copy the package nl.jteam.rootmount to your own project and wire it up similarly as in the demo's WicketApplication. In addition you will need to remove a 'final' keyword from the Wicket class BookmarkablePageRequestTargetUrlCodingStrategy. See section 'Implementing RootMountedUrlCodingStrategy' above for more information.

If you implement your own root URL strategies, take note that the accept method might be called quite often. It is prudent to make sure this method returns quickly.

Conclusion

This article shows that it is possible to have root mounts in Wicket 1.4. The downloadable demo application shows that it is even possible with a simple API. However, the implementation has to jump through some hoops to make this possible, and some of these hoops are not pretty at all. Luckily starting with Wicket 1.4.8 the biggest obstacle (building your own Wicket release) has been removed.

Download the demo

wicket-rootmount-demo.tar.gz

10 Responses

  1. February 25, 2010 at 17:38 by Nishant

    Hey Erik,

    Many thanks; was looking for this.

    - Nishant

  2. February 26, 2010 at 15:10 by Ludger

    Hello Erik,

    Thanks for sharing the code. I was looking for this.

    Regards
    Ludger

  3. April 7, 2010 at 20:57 by Alex

    Hi Erik!
    Thank you for this great post. One question, why BookmarkablePageRequestTargetUrlCodingStrategy#encode method is still final even in wicket-1.4.7 if there is a good use-case for extending it?

    Also, maybe you could add this contribution to wicket core or at least wicket-extension?

    Thank you!
    Alex Objelean

    [Update: Erik corrected spelling of his name.]

  4. April 12, 2010 at 08:51 by Erik van Oosten

    Hi Alex,

    Thanks for creating issue https://issues.apache.org/jira/browse/WICKET-2830! I did not expect the final keyword to be removed so quickly!

    Regards,
    Erik.

  5. April 14, 2010 at 00:09 by Aditi Mohan

    Thanks Erik! This was very helpful. I had a similar requirement and this helped me get to the solution w/o getting much in details of Wicket framework.
    Thanks for the post again.

  6. May 3, 2010 at 02:17 by Ondra Zizka

    Hi, I posted about a similar topic at
    http://apache-wicket.1842946.n4.nabble.com/Path-based-UrlCodingStrategy-td2123342.html#a2123387
    (just linking if someone was interested :)

  7. July 23, 2010 at 09:19 by Erik van Oosten

    Added a link to https://cwiki.apache.org/confluence/display/WICKET/Wicket+and+localized+URLs as an alternative to URL rewriting.

  8. July 25, 2010 at 20:56 by ignazio

    Hi,
    I've just d'loaded the tar.gz and the mvn jetty:run command loads a jetty instance whose root url is the $project.name, therefore the links on the first page simply doesn't work (at least to me), but ,once added the / a the jetty plugin config, everything worked fine

    your post was very useful

    regards

    I

  9. July 26, 2010 at 08:28 by Erik van Oosten

    Thanks Ignazio. I tested the application by starting the class Start from an IDE. You don't have a context path then.

  10. April 22, 2011 at 12:42 by Alex

    Nice article!

    Would be nice to have a follow-up with how "Wicket 1.5 makes this easy" now that it's nearly out (second release candidate 1.5-rc2 has already hit the maven repositories).

    :-)

Leave a Reply