Wednesday, October 15, 2008

Wicket extreme consistent URLs

Setting up a page to be behind a particular URL (aka mounting) in Wicket is fairly easy. Daan recently wrote REST like URLs for Wicket in which this is nicely explained. However, consistently keeping nice URLs, for example after a form submit, is a whole lot harder. So hard in fact, that even Wicket champion Igor believes it is currently not possible (it is an old post). His reasoning is of course completely correct, but he forgot one thing. Let me explain.

The problem
Many web applications have a login page. On it there is a form where you fill in your username and password. Suppose the page is mounted to:

http://example.com/login
with code like this in the application's init method:
mountBookmarkablePage("login", LoginPage.class);
When the user enters a wrong password, form validation (I assume you have a password validator) will fail and Wicket will redirect the user to a URL that points to the second version of the login page (the one with the error message). These URLs typically look like:
http://example.com/?wicket:interface=:0:::
For many applications this is just fine, in some its just too ugly.

A non solution
One of the proposed solutions is that you redirect to another page in the onError method of the form. E.g.

add(new Form(...) { @Override protected void onError() { setResponsePage(LoginPage.class); } }

The redirection will happen, and the URL is indeed that of the login page, but you will have no error messages. Instead of going to the second version of the login page, you have created a new instance of the login page!

Another attempt
We could pass the error code in the URL:

add(new Form(...) { @Override protected void onError() { PageParameters pp = new PageParameters(); pp.setString("error", "wronglogin"); setResponsePage(new LoginPage(pp)); } }

Unfortunately, with the default URL encoding stratey the URL will now be:
http://example.com/login/error/wronglogin
Not at all attractive.

Getting close
A fairly recent addition to Wicket is the HybridUrlCodingStrategy (and subclasses). Let's mount the login page with one of these:

mount(new HybridUrlCodingStrategy("login", LoginPage.class));

If a user now enters wrong credentials, Wicket will redirect you to
http://example.com/login.2
The .2 means the second version of the login page for the current session. If the user would delete the .2 from the URL, the HybridUrlCodingStrategy will find the latest available version of the page and serve that. A whole lot nicer but still not perfect.

The solution
If only if we could force Wicket to not display the version number. Well... we can! We'll have to do some coding first:

// First attempt, DO NOT USE public class NonVersionedHybridUrlCodingStrategy extends HybridUrlCodingStrategy { // ... trivial ctor @Override protected String addPageInfo( String url, PageInfo pageInfo) { // Do not add the version number as // super.addPageInfo would do. return url; } }

And now mount with:
mount(new NonVersionedHybridUrlCodingStrategy( "login", LoginPage.class));
Indeed, the version number is gone! However, there is still a tweak to be done. In some circumstance Wicket will redirect you to the latest version of the page, but now that redirect will be to the same URL. This is no problem for Firefox and IE, but Safari, Opera and many tools do not allow this. Here is the final version that does not have the problem:
/** * UrlCodingStrategy that will give the same * URL for every version of a page. * @author Erik van Oosten */ public class NonVersionedHybridUrlCodingStrategy extends HybridUrlCodingStrategy { public NonVersionedHybridUrlCodingStrategy( String mountPath, Class pageClass) { super(mountPath, pageClass, false); } @Override protected String addPageInfo( String url, PageInfo pageInfo) { // Do not add the version number as // super.addPageInfo would do. return url; } }

I named the URL strategy 'non versioned' as it no longer makes sense to have multiple versions of a page, just the last one will do. You should therefore also add the following fragment to each page constructor:
setVersioned(false);

Proof
Try it out! Go to tipSpot.com and try to login.