Earlier versions of Tapestry have had a long-standing tradition of really ugly URLs . Because the framework generates the URLs and is also responsible for parsing and dispatching on them in later requests, it was not seen as an issue.
In fact, the ugly URLs do cause some problems:
/app
), J2EE declarative security, which is path-based, is defeated.
/app?page=news/Thread&service=page
may be converted into the friendly URL
/news/Threads.html
. In this case, the
page=news/Thread
query parameter became the
news/Thread
portion of the URL, and the
service=page
query parameter became the
.html
extension to the URL.
service
query parameter is used to select an engine service by name. A number of
services are provided with the framework, the most common of which are:
Each service is responsible for creating URLs with the correct query parameters.
By default, the URL path is always
/app
and any additional information comes out of the query parameters. The most
common parameters are:
Following this scheme, a typical URL might be
/app?component=border.logout&page=news/Thread&service=direct
. Yep, that's UGLY.
To use ordinary ugly URLs, Tapestry requires only a small amount of configuration in web.xml . Enabling friendly URLs requires adding more configuration to web.xml, and to your HiveMind module deployment descriptor .
Friendly URLs are controlled by ServiceEncoder s. Getting Tapestry to output friendly URLs is a matter of plugging encoders into the correct pipeline ... this is all done using HiveMind.
The most common type of encoder is the
page-service-encoder
, which encodes the
page
and
service
parameters. In your hivemodule.xml:
<contribution configuration-id="tapestry.url.ServiceEncoders"> <page-service-encoder id="page" extension="html" service="page"/> </contribution>
This contribution to the
tapestry.url.ServiceEncoders
configuration point creates a
ServiceEncoder
that maps the
.html
extension (on the URL path) to the page service. The
id
attribute must be unique for all contributed encoders.
For Tapestry to recognize the URLs, you must inform the servlet container to send them to the Tapestry application servlet, by adding a mapping to web.xml:
<servlet-mapping> <servlet-name>myapp</servlet-name> <url-pattern>*.html</url-pattern> </servlet-mapping>
This means that even static HTML pages that are part of your web application will be treated as Tapestry pages; any incoming request that ends with .html will be routed into the Tapestry application. Page specifications are optional, so Tapestry will treat the HTML pages are if they were HTML page templates. If you want to allow ordinary static content, then you should use another extension such as ".page" or ".tap" (the choice is arbitrary).
A specialized encoder used exclusively with the direct service. Encodes the page name into the servlet path, then a comma, then the nested id for the component. One of two extensions is used, depending on whether the URL is stateful (an HttpSession existed when the link was rendered), or stateless.
A typical URL might be:
/admin/Menu,border.link.direct
. This indicates a page name of
admin/Menu
and a component id of
border.link
. By convention, the ".direct" extension is for stateless URLs.
The hivemodule.xml contribution:
<contribution configuration-id="tapestry.url.ServiceEncoders"> <direct-service-encoder id="direct" stateless-extension="direct" stateful-extension="sdirect"/> </contribution>
In addition, the
*.direct
and
*.sdirect
mappings must be added to web.xml:
<servlet-mapping> <servlet-name>myapp</servlet-name> <url-pattern>*.direct</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>myapp</servlet-name> <url-pattern>*.sdirect</url-pattern> </servlet-mapping>
The
asset-encoder
is for use with the asset service. The asset service exposes assets stored
on the classpath (i.e., inside JARs) to the client web browser. The asset
service receives a request with a resource path, and writes back a binary
stream of that resources content.
In addition, each request includes a message digest , a string generated from the bytes of the the resource. This message digest acts as a credential , assuring that only classpath resources explicitly exposed by the application are accessible by the client (this prevents devious users from obtaining Java class files, for example). The message digest can only be computed by the server, using the full content of the actual file.
To enable friendly URLs for the asset service, add the following to your hivemodule.xml:
<contribution configuration-id="tapestry.url.ServiceEncoders"> <asset-encoder id="asset" path="/assets"/> </contribution>
This contribution will encode asset URLs using the given path. The provided
path,
/assets
comes first, then the digest string, then the path for the URL. An example
URI would be
/assets/91ab6d51232df0384663312f405babbe/org/apache/tapestry/contrib/palette/select_right.gif
.
In addition you must add a mapping to web.xml:
<servlet-mapping> <servlet-name>myapp</servlet-name> <url-pattern>/assets/*</url-pattern> </servlet-mapping>
If you choose a different folder than
/assets/
then be sure to make corresponding changes in both hivemodule.xml and
web.xml.
The
extension-encoder
is used to encode just the
service
query parameter. The output URL is the service name with a fixed extension
(typically, ".svc"), i.e.,
/home.svc
or
/restart.svc
.
In your hivemodule.xml:
<contribution configuration-id="tapestry.url.ServiceEncoders"> <extension-encoder id="extension" extension="svc" after="*"/> </contribution>
The use of the
after
attribute ensures that this encoder is always executed after any other
encoders. Order is important!
For this example, another mapping is required in the web.xml:
<servlet-mapping> <servlet-name>myapp</servlet-name> <url-pattern>*.svc</url-pattern> </servlet-mapping>
Finally, when one of the pre-defined encoders is insufficient, you can define your own. The <encoder> element allows an arbitrary object that implements the ServiceEncoder interface to be plugged into the pipeline. The <encoder> element supports the (required) id attribute, and the optional before and after attributes.
From the Virtual Library example, a custom encoder implementation is used as
a special way to reference the ViewBook and ViewPerson pages using the
external service (see the
ExternalLink
component for more information about using this engine service). The end
result is that the URLs for these two pages look like
/vlib/book/2096
rather than
/vlib/ViewBook.external?sp=2096
or
/vlib/app?page=ViewBook&service=external&sp=2096
. Certainly the first option is by far the prettiest.
These encoders are configured in hivemodule.xml as follows:
<encoder id="viewbook" before="external" object="instance:ViewPageEncoder,pageName=ViewBook,url=/book"/> <encoder id="viewperson" before="external" object="instance:ViewPageEncoder,pageName=ViewPerson,url=/person"/> <page-service-encoder id="external" extension="external" service="external"/>
The order of the encoders in the pipline is very important, so the use of the before attribute ensures that the specialized encoders for these two pages are allowed to operate before the general purpose external service encoder.
The two special pages are mapped in web.xml using their custom URLs:
<servlet-mapping> <servlet-name>vlib</servlet-name> <url-pattern>/book/*</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>vlib</servlet-name> <url-pattern>/person/*</url-pattern> </servlet-mapping>
The implementation of the ViewPageEncoder class is all about an encode() method and a matching decode() method.
The encode() method must check to see if the link being generated is the right page name and the right service, returning (without doing anything) if not. The link being constructed is represented as an instance of ServiceEncoding :
public void encode(ServiceEncoding encoding) { if (!isExternalService(encoding)) return; String pageName = encoding.getParameterValue(ServiceConstants.PAGE); if (!pageName.equals(_pageName)) return; StringBuilder builder = new StringBuilder(_url); String[] params = encoding.getParameterValues(ServiceConstants.PARAMETER); // params will not be null; in fact, pretty sure it will consist // of just one element (an integer). for (String param : params) { builder.append("/"); builder.append(param); } encoding.setServletPath(builder.toString()); encoding.setParameterValue(ServiceConstants.SERVICE, null); encoding.setParameterValue(ServiceConstants.PAGE, null); encoding.setParameterValue(ServiceConstants.PARAMETER, null); } private boolean isExternalService(ServiceEncoding encoding) { String service = encoding.getParameterValue(ServiceConstants.SERVICE); return service.equals(Tapestry.EXTERNAL_SERVICE); }
We cheat just a bit here because we know that the service parameters will be a single numeric string. You can see exactly how encoder works, by building a new servlet path that encodes information that was stored as query parameters, the setting those query parameters to null
The flip side is the decode() method, which works by recognizing the URL generated by the encode() method and restoring the query parameters by parsing the URL:
public void decode(ServiceEncoding encoding) { String servletPath = encoding.getServletPath(); if (!servletPath.equals(_url)) return; String pathInfo = encoding.getPathInfo(); String[] params = TapestryUtils.split(pathInfo.substring(1), '/'); encoding.setParameterValue(ServiceConstants.SERVICE, Tapestry.EXTERNAL_SERVICE); encoding.setParameterValue(ServiceConstants.PAGE, _pageName); encoding.setParameterValues(ServiceConstants.PARAMETER, params); }
When constructing this style of encoder, it is important to remember that the servlet path does not end with a slash, but tthe path info, if non-null, will start with a slash.