10. Wicket Links and URL generation

Up to now we used component Link to move from a page to another and we have seen that it is quite similar to a “click” event handler (see paragraph 4.4).

However this component alone is not enough to build all possible kinds of links we may need in our pages. Therefore, Wicket offers other link components suited for those tasks which can not be accomplished with a basic Link.

Besides learning new link components, in this chapter we will also see how to customize the page URL generated by Wicket using the encoding facility provided by the framework and the page parameters that can be passed to a target page.

10.1. PageParameters

A common practice in web development is to pass data to a page using query string parameters (like ?paramName1=paramValu1&paramName2=paramValue2…​). Wicket offers a more flexible and object oriented way to do this with models (we will see them in the next chapter). However, even if we are using Wicket, we still need to use query string parameters to exchange data with other Internet-based services. Consider for example a classic confirmation page which is linked inside an email to let users confirm important actions like password changing or the subscription to a mailing list. This kind of page usually expects to receive a query string parameter containing the id of the action to confirm.

Query string parameters can also be referred to as named parameters. In Wicket they are handled with class org.apache.wicket.request.mapper.parameter.PageParameters. Since named parameters are basically name-value pairs, PageParameters works in much the same way as Java Map providing two methods to create/modify a parameter (add(String name, Object value) and set(String name, Object value)), one method to remove an existing parameter (remove(String name)) and one to retrieve the value of a given parameter (get(String name)) . Here is a snippet to illustrate the usage of PageParameters:

  1. PageParameters pageParameters = new PageParameters();
  2. //add a couple of parameters
  3. pageParameters.add("name", "John");
  4. pageParameters.add("age", 28);
  5. //retrieve the value of 'age' parameter
  6. pageParameters.get("age");

Now that we have seen how to work with page parameters, let’s see how to use them with our pages.

10.1.1. PageParameters and bookmarkable pages

Base class Page comes with a constructor which takes as input a PageParameters instance. If we use this superclass constructor in our page, PageParameters will be used to build the page URL and it can be retrieved at a later time with the Page’s getPageParameters() method.

In the following example taken from the PageParametersExample project we have a home page with a link to a second page that uses a version of setResponsePage method that takes as input also a PageParameters to build the target page (named PageWithParameters). The code for the link and for the target page is the following:

Link code:

  1. add(new Link<Void>("pageWithIndexParam") {
  2. @Override
  3. public void onClick() {
  4. PageParameters pageParameters = new PageParameters();
  5. pageParameters.add("foo", "foo");
  6. pageParameters.add("bar", "bar");
  7. setResponsePage(PageWithParameters.class, pageParameters);
  8. }
  9. });

Target page code:

  1. public class PageWithParameters extends WebPage {
  2. //Override superclass constructor
  3. public PageWithParameters(PageParameters parameters) {
  4. super(parameters);
  5. }
  6. }

The code is quite straightforward and it’s more interesting to look at the URL generated for the target page:

  1. <app root>/PageParametersExample/wicket/bookmarkable/
  2. org.wicketTutorial.PageWithParameters?foo=foo&bar=bar

At first glance the URL above could seem a little weird, except for the last part which contains the two named parameters used to build the target page.

The reason for this “strange” URL is that, as we explained in paragraph 8.3, when a page is instantiated using a constructor with no argument or using a constructor that accepts only a PageParameters, Wicket will try to generate a static URL for it, with no session-relative informations. This kind of URL is called bookmarkable because it can be saved by the users as a bookmark and accessed at a later time.

A bookmarkable URL is composed by a fixed prefix (which by default is bookmarkable) and the qualified name of the page class (org.wicketTutorial.PageWithParameters in our example). Segment wicket is another fixed prefix added by default during URL generation. In paragraph 10.6 we will see how to customize fixed prefixes with a custom implementation of IMapperContext interface.

10.1.2. Indexed parameters

Besides named parameters, Wicket also supports indexed parameters. These kinds of parameters are rendered as URL segments placed before named parameters. Let’s consider for example the following URL:

  1. <application path>/foo/bar?1&baz=baz

The URL above contains two indexed parameters (foo and bar) and a query string consisting of the page id and a named parameter (baz). Just like named parameters also indexed parameters are handled by the PageParameters class. The methods provided by PageParameters for indexed parameters are set(int index, Object object) (to add/modify a parameter), remove(int index)(to remove a parameter) and get(int index) (to read a parameter).

As their name suggests, indexed parameters are identified by a numeric index and they are rendered following the order in which they have been added to the PageParameters. The following is an example of indexed parameters:

  1. PageParameters pageParameters = new PageParameters();
  2. //add a couple of parameters
  3. pageParameters.set(0, "foo");
  4. pageParameters.set(1, "bar");
  5. //retrieve the value of the second parameter ("bar")
  6. pageParameters.get(1);

Project PageParametersExample comes also with a link to a page with both indexed parameters and a named parameter:

  1. add(new Link<Void>("pageWithNamedIndexParam") {
  2. @Override
  3. public void onClick() {
  4. PageParameters pageParameters = new PageParameters();
  5. pageParameters.set(0, "foo");
  6. pageParameters.set(1, "bar");
  7. pageParameters.add("baz", "baz");
  8. setResponsePage(PageWithParameters.class, pageParameters);
  9. }
  10. });

The URL generated for the linked page (PageWithParameters) is the one seen at the beginning of the paragraph.

10.2. Bookmarkable links

A link to a bookmarkable page can be built with the link component org.apache.wicket.markup.html.link.BookmarkablePageLink:

  1. BookmarkablePageLink bpl=new BookmarkablePageLink<Void>(PageWithParameters.class, pageParameters);

The specific purpose of this component is to provide an anchor to a bookmarkable page, hence we don’t have to implement any abstract method like we do with Link component.

10.3. Automatically creating bookmarkable links with tag wicket:link

Bookmarkable pages can be linked directly inside markup files without writing any Java code. Using tag we ask Wicket to automatically add bookmarkable links for the anchors wrapped inside it. Here is an example of usage of tag taken from the home page of the project BookmarkablePageAutoLink:

  1. <!DOCTYPE html>
  2. <html xmlns:wicket="http://wicket.apache.org">
  3. <head>
  4. <meta charset="utf-8" />
  5. <title>Apache Wicket Quickstart</title>
  6. </head>
  7. <body>
  8. <div id="bd">
  9. <wicket:link>
  10. <a href="HomePage.html">HomePage</a><br/>
  11. <a href="anotherPackage/SubPackagePage.html">SubPackagePage</a>
  12. </wicket:link>
  13. </div>
  14. </body>
  15. </html>

The key part of the markup above is the href attribute which must contain the package-relative path to a page. The home page is inside package org.wicketTutorial which in turns contains the sub package anotherPackage. This package hierarchy is reflected by the href attributes: in the first anchor we have a link to the home page itself while the second anchor points to page SubPackagePage which is placed into sub package anotherPackage. Absolute paths are supported as well and we can use them if we want to specify the full package of a given page. For example the link to SubPackagePage could have been written in the following (more verbose) way:

  1. <a href="/org/wicketTutorial/anotherPackage/SubPackagePage.html"> SubPackagePage</a>

If we take a look also at the markup of SubPackagePage we can see that it contains a link to the home page which uses the parent directory selector (relative path):

  1. <!DOCTYPE html>
  2. <html xmlns:wicket="http://wicket.apache.org">
  3. <head>
  4. <meta charset="utf-8" />
  5. <title>Apache Wicket Quickstart</title>
  6. </head>
  7. <body>
  8. <div id="bd">
  9. <wicket:link>
  10. <a href="../HomePage.html">HomePage</a><br/>
  11. <a href="SubPackagePage.html">SubPackagePage</a>
  12. </wicket:link>
  13. </div>
  14. </body>
  15. </html>

Please note that any link to the current page (aka self link) is disabled. For example in the home page the self link is rendered like this:

  1. <span><em>HomePage</em></span>

The markup used to render disabled links can be customized using the markup settings (class org.apache.wicket.settings.MarkupSettings) available in the application class:

  1. @Override
  2. public void init()
  3. {
  4. super.init();
  5. //wrap disabled links with <b> tag
  6. getMarkupSettings().setDefaultBeforeDisabledLink("<b>");
  7. getMarkupSettings().setDefaultAfterDisabledLink("</b>");
  8. }

The purpose of tag is not limited to just simplifying the usage of bookmarkable pages. As we will see in chapter 13, this tag can also be adopted to manage web resources like pictures, CSS files, JavaScript files and so on.

10.4. External links

Since Wicket uses plain HTML markup files as templates, we can place an anchor to an external page directly inside the markup file. When we need to dynamically generate external anchors, we can use link component org.apache.wicket.markup.html.link.ExternalLink. In order to build an external link we must specify the value of the href attribute using a model or a plain string. In the next snippet, given an instance of Person, we generate a Google search query for its full name:

Html:

  1. <a wicket:id="externalSite">Search me on Google!</a>

Java code:

  1. Person person = new Person("John", "Smith");
  2. String fullName = person.getFullName();
  3. //Space characters must be replaced by character '+'
  4. String googleQuery = "http://www.google.com/search?q=" + fullName.replace(" ", "+");
  5. add(new ExternalLink("externalSite", googleQuery));

Generated anchor:

  1. <a href="http://www.google.com/search?q=John+Smith">Search me on Google!</a>

If we need to specify a dynamic value for the text inside the anchor, we can pass it as an additional constructor parameter:

Html:

  1. <a wicket:id="externalSite">Label goes here...</a>

Java code:

  1. Person person = new Person("John", "Smith");
  2. String fullName = person.getFullName();
  3. String googleQuery = "http://www.google.com/search?q=" + fullName.replace(" ", "+");
  4. String linkLabel = "Search '" + fullName + "' on Google.";
  5. add(new ExternalLink("externalSite", googleQuery, linkLabel));

Generated anchor:

  1. <a href="http://www.google.com/search?q=John+Smith">Search 'John Smith' on Google.</a>

10.5. Stateless links

Component Link has a stateful nature, hence it cannot be used with stateless pages. To use links with these kinds of pages Wicket provides the convenience org.apache.wicket.markup.html.link.StatelessLink component which is basically a subtype of Link with the stateless hint set to true.

Please keep in mind that Wicket generates a new instance of a stateless page also to serve stateless links, so the code inside the onClick() method can not depend on instance variables. To illustrate this potential issue let’s consider the following code (from the project StatelessPage) where the value of the variable index is used inside onclick():

  1. public class StatelessPage extends WebPage {
  2. private int index = 0;
  3. public StatelessPage(PageParameters parameters) {
  4. super(parameters);
  5. }
  6. @Override
  7. protected void onInitialize() {
  8. super.onInitialize();
  9. setStatelessHint(true);
  10. add(new StatelessLink("statelessLink") {
  11. @Override
  12. public void onClick() {
  13. //It will always print zero
  14. System.out.println(index++);
  15. }
  16. });
  17. }
  18. }

The printed value will always be zero because a new instance of the page is used every time the user clicks on the statelessLink link.

10.6. Generating structured and clear URLs

Having structured URLs in our site is a basic requirement if we want to build an efficient SEO strategy, but it also contributes to improve user experience with more intuitive URLs. Wicket provides two different ways to control URL generation. The first (and simplest) is to “mount” one or more pages to an arbitrary path, while a more powerful technique is to use custom implementations of IMapperContext and IPageParametersEncoder interfaces. In the next paragraphs we will learn both of these two techniques.

10.6.1. Mounting a single page

With Wicket we can mount a page to a given path in much the same way as we map a servlet filter to a desired path inside file web.xml (see paragraph 4.2). Using mountPage(String path, Class pageClass) method of the WepApplication class we tell Wicket to respond with a new instance of pageClass whenever a user navigates to the given path. In the application class of the project MountedPagesExample we mount MountedPage to the “/pageMount” path:

  1. @Override
  2. public void init()
  3. {
  4. super.init();
  5. mountPage("/pageMount", MountedPage.class);
  6. //Other initialization code...
  7. }

The path provided to mountPage will be used to generate the URL for any page of the specified class:

  1. //it will return "/pageMount"
  2. RequestCycle.get().urlFor(MountedPage.class);

Under the hood the mountPage method mounts an instance of the request mapper org.apache.wicket.request.mapper.MountedMapper configured for the given path:

  1. public final <T extends Page> void mountPage(final String path,final Class<T> pageClass) {
  2. mount(new MountedMapper(path, pageClass));
  3. }

Request mappers and the Application’s method mount have been introduced in the previous chapter (paragraph 9.3).

10.6.2. Using parameter placeholders with mounted pages

The path specified for mounted pages can contain dynamic segments which are populated with the values of the named parameters used to build the page. These segments are declared using special segments called parameter placeholders. Consider the path used in the following example:

  1. mountPage("/pageMount/${foo}/otherSegm", MountedPageWithPlaceholder.class);

The path used above is composed by three segments: the first and the last are fixed while the second will be replaced by the value of the named parameter foo that must be provided when the page MountedPageWithPlaceholder is instantiated:

Java code:

  1. PageParameters pageParameters = new PageParameters();
  2. pageParameters.add("foo", "foo");
  3. setResponsePage(MountedPageWithPlaceholder.class, pageParameters)

Generated URL:

  1. <Application path>/pageMount/foo/otherSegm

On the contrary if we manually insert an URL like ‘/pageMount/bar/otherSegm’, we can read value ‘bar’ retrieving the named parameter foo inside our page.

Place holders can be declared as optional using the ‘#’ character in place of ‘$’:

  1. mountPage("/pageMount/#{foo}/otherSegm", MountedPageOptionalPlaceholder.class);

If the named parameter for an optional placeholder is missing, the corresponding segment is removed from the final URL:

Java code:

  1. PageParameters pageParameters = new PageParameters();
  2. setResponsePage(MountedPageWithPlaceholder.class, pageParameters);

Generated URL:

  1. <Application path>/pageMount/otherSegm

10.6.3. Mounting a package

In addition to mounting a single page, Wicket allows to mount all of the pages inside a package to a given path. Method mountPackage(String path, Class pageClass) of class WepApplication will mount every page inside pageClass’s package to the specified path.

The resulting URL for package-mounted pages will have the following structure:

  1. <Application path>/mountedPath/<PageClassName>[optional query string]

For example in the MountedPagesExample project we have mounted all pages inside the subpackage org.tutorialWicket.subPackage with this line of code:

  1. mountPackage("/mountPackage", StatefulPackageMount.class);

StatefulPackageMount is one of the pages placed into the desired package and its URL will be:

  1. <Application path>/mountPackage/StatefulPackageMount?1

Similarly to what is done by the mountPage method, the implementation of the mountPackage method mounts an instance of org.apache.wicket.request.mapper.PackageMapper to the given path.

10.6.4. Providing custom mapper context to request mappers

Interface org.apache.wicket.request.mapper.IMapperContext is used by request mappers to create new page instances and to retrieve static URL segments used to build and parse page URLs. Here is the list of these segments:

  • Namespace: it’s the first URL segment of non-mounted pages. By default its value is wicket.

  • Identifier for non-bookmarkable URLs: it’s the segment that identifies non bookmarkable pages. By default its value is page.

  • Identifier for bookmarkable URLs: it’s the segment that identifies bookmarkable pages. By default its value is bookmarkable (as we have seen before in paragraph 10.1.1).

  • Identifier for resources: it’s the segment that identifies Wicket resources. Its default value is resources. The topic of resource management will be covered in chapter 16.

IMapperContext provides a getter method for any segment listed above. By default Wicket uses class org.apache.wicket.DefaultMapperContext as mapper context.

Project CustomMapperContext is an example of customization of mapper context where we use index as identifier for non-bookmarkable pages and staticURL as identifier for bookmarkable pages. In this project, instead of implementing our mapper context from scratch, we used DefaultMapperContext as base class overriding just the two methods we need to achieve the desired result (getBookmarkableIdentifier() and getPageIdentifier()). The final implementation is the following:

  1. public class CustomMapperContext extends DefaultMapperContext{
  2. @Override
  3. public String getBookmarkableIdentifier() {
  4. return "staticURL";
  5. }
  6. @Override
  7. public String getPageIdentifier() {
  8. return "index";
  9. }
  10. }

Now to use a custom mapper context in our application we must override the newMapperContext() method declared in the Application class and make it return our custom implementation of IMapperContext:

  1. @Override
  2. protected IMapperContext newMapperContext() {
  3. return new CustomMapperContext();
  4. }

10.6.5. Controlling how page parameters are encoded with IPageParametersEncoder

Some request mappers (like MountedMapper and PackageMapper) can delegate page parameters encoding/decoding to interface org.apache.wicket.request.mapper.parameter.IPage ParametersEncoder. This entity exposes two methods: encodePageParameters() and decodePageParameters(): the first one is invoked to encode page parameters into an URL while the second one extracts parameters from the URL.

Wicket comes with a built-in implementation of this interface which encodes named page parameters as URL segments using the following pattern: /paramName1/paramValue1/paramName2/param Value2…​

This built-in encoder is org.apache.wicket.request.mapper.parameter.UrlPathPageParametersEncoder class. In the PageParametersEncoderExample project we have manually mounted a MountedMapper that takes as input also an UrlPathPageParametersEncoder:

  1. @Override
  2. public void init() {
  3. super.init();
  4. mount(new MountedMapper("/mountedPath", MountedPage.class, new UrlPathPageParametersEncoder()));
  5. }

The home page of the project contains just a link to the MountedPage web page. The code of the link and the resulting page URL are:

Link code:

  1. add(new Link<Void>("mountedPage") {
  2. @Override
  3. public void onClick() {
  4. PageParameters pageParameters = new PageParameters();
  5. pageParameters.add("foo", "foo");
  6. pageParameters.add("bar", "bar");
  7. setResponsePage(MountedPage.class, pageParameters);
  8. }
  9. });

Generated URL:

  1. <Application path>/mountedPath/foo/foo/bar/bar?1

10.6.6. Encrypting page URLs

Sometimes URLs are a double–edged sword for our site because they can expose too many details about the internal structure of our web application making it more vulnerable to malicious users.

To avoid this kind of security threat we can use the CryptoMapper request mapper which wraps an existing mapper and encrypts the original URL producing a single encrypted segment:

url encrypted

Typically, CryptoMapper is registered into a Wicket application as the root request mapper wrapping the default one:

  1. @Override
  2. public void init() {
  3. super.init();
  4. setRootRequestMapper(new CryptoMapper(getRootRequestMapper(), this));
  5. //pages and resources must be mounted after we have set CryptoMapper
  6. mountPage("/foo/", HomePage.class);

As pointed out in the code above, pages and resources must be mounted after having set CryptoMapper as root mapper, otherwise the mounted paths will not work.

By default CryptoMapper encrypts page URLs with a cipher that might not be strong enough for production environment. Paragraph Security with Wicket will provide a more detailed description of how Wicket encrypts page URLs and we will see how to use stronger ciphers.

10.7. Summary

Links and URLs are not trivial topics as they may seem and in Wicket they are strictly interconnected. Developers must choose the right trade-off between producing structured URLs and avoiding to make them verbose and vulnerable.

In this chapter we have explored the tools provided by Wicket to control how URLs are generated. We have started with static URLs for bookmarkable pages and we have seen how to pass parameters to target pages with PageParameters. In the second part of the chapter we focused on mounting pages to a specific path and on controlling how parameters are encoded by Wicket. Finally, we have also seen how to encrypt URLs to prevent security vulnerabilities.