blob: a0746610553d3fbeae2eaf9b092613e2450866e0 [file] [log] [blame]
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE document PUBLIC "//xdoc/" "http://maven.apache.org/dtd/xdoc_1_0.dtd">
<!--
Copyright 2005 The Apache Software Foundation
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<document>
<properties>
<title>Configuring Friendly URLs</title>
</properties>
<body>
<section name="Configuring Friendly URLs">
<p>
Earlier versions of Tapestry have had a long-standing tradition of
<em>really ugly URLs</em>
. 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.
</p>
<p>
In fact, the ugly URLs
<em>do</em>
cause some problems:
</p>
<ul>
<li>
Since all requests are routed through a single servlet (typically mapped to
<code>/app</code>
), J2EE declarative security, which is path-based, is defeated.
</li>
<li>
Ugly URLs tend to be longer than friendly URLs, which can make a difference when
creating a WML application.
</li>
<li>
A single directory may contain all the artifacts (HTML templates,
specifications, properties files) for all the pages in an entire application.
There isn't a sanctioned approach to organizing things into subdirectories.
</li>
<li>
The reliance on query parameters means that common search engines will only see
a tiny fraction of the application.
</li>
</ul>
<p>
Starting with 4.0,
<em>friendly URLs</em>
are integrated directly into framework (in 3.0 an ambitious, but more limited, patch
was required).
</p>
<span class="warn">
<strong>Warning:</strong>
<p>
For security purposes, enabling friendly URLs implies that pages are no longer
accessible via their ugly URL counterpart. This is not the case. If a malevolent
user can either guess - or via cookies identify - your servlet path, they can
construct an ugly URL to a resource that is protected via security and gain access
to the protected resource.
</p>
</span>
<p>Friendly URLs are divided into two concerns:</p>
<ul>
<li>
Converting information normally stored as a query parameter into part of the URL
path.
</li>
<li>Parsing the path to restore the information previously encoded.</li>
</ul>
<p>
For example, the ugly URL
<code>/app?page=news/Thread&amp;service=page</code>
may be converted into the friendly URL
<code>/news/Threads.html</code>
. In this case, the
<code>page=news/Thread</code>
query parameter became the
<code>news/Thread</code>
portion of the URL, and the
<code>service=page</code>
query parameter became the
<code>.html</code>
extension to the URL.
</p>
<subsection name="Understanding Tapestry URLs">
<p>
To understand how to get friendly URLs, you must understand a little about what
information Tapestry packs into URLs.
</p>
<p>
Every request to a Tapestry application is mapped to an
<em>engine service</em>
. An engine service is something like a servlet, embedded within Tapestry. The
<code>service</code>
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:
</p>
<dl>
<dt>page</dt>
<dd>Activates and renders a specific page.</dd>
<dt>direct</dt>
<dd>
Used with the
<a href="../components/link/directlink.html">DirectLink</a>
and
<a href="../components/form/form.html">Form</a>
components.
</dd>
<dt>home</dt>
<dd>
Default service used when the service parameter is not specified (such as
when first accessing the application); activates and renders the Home page.
</dd>
</dl>
<p>
Each service is responsible for creating URLs with the correct query parameters.
By default, the URL path is always
<code>/app</code>
and any additional information comes out of the query parameters. The most
common parameters are:
</p>
<dl>
<dt>page</dt>
<dd>The name of a page to activate.</dd>
<dt>service</dt>
<dd>The service responsible for the request.</dd>
<dt>component</dt>
<dd>The nested component id of a component.</dd>
<dt>
sp
</dt>
<dd>
Stores listener parameters passed in the URL (used by
<a href="../components/link/directlink.html">DirectLink</a>
and passed into
<a href="listenermethods.html">listener method</a>
s, the "sp" is a holdover from 3.0).
</dd>
</dl>
<p>
Following this scheme, a typical URL might be
<code>
/app?component=border.logout&amp;page=news/Thread&amp;service=direct
</code>
. Yep, that's UGLY.
</p>
</subsection>
<subsection name="Enabling Friendly URLs">
<p>
To use ordinary ugly URLs, Tapestry requires only a
<a href="configuration.html#configuration.deployment-descriptor">
small amount of configuration in web.xml
</a>
. Enabling friendly URLs requires adding more configuration to web.xml, and to
your
<a href="hivemind.html">HiveMind module deployment descriptor</a>
.
</p>
<p>
Friendly URLs are controlled by
<a
href="../apidocs/org/apache/tapestry/engine/ServiceEncoder.html">
ServiceEncoder
</a>
s. Getting Tapestry to output friendly URLs is a matter of plugging encoders
into the correct pipeline ... this is all done using HiveMind.
</p>
<subsection name="page-service-encoder">
<p>
The most common type of encoder is the
<code>page-service-encoder</code>
, which encodes the
<code>page</code>
and
<code>service</code>
parameters. In your hivemodule.xml:
</p>
<source xml:space="preserve">
&lt;contribution configuration-id="tapestry.url.ServiceEncoders"&gt;
&lt;page-service-encoder id="page" extension="html" service="page"/&gt;
&lt;/contribution&gt;</source>
<p>
This contribution to the
<a href="../tapestry-framework/hivedoc/config/tapestry.url.ServiceEncoders.html">
tapestry.url.ServiceEncoders
</a>
configuration point creates a
<a
href="../apidocs/org/apache/tapestry/engine/ServiceEncoder.html">
ServiceEncoder
</a>
that maps the
<code>.html</code>
extension (on the URL path) to the page service. The
<code>id</code>
attribute must be unique for all contributed encoders.
</p>
<p>
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:
</p>
<source xml:space="preserve">
&lt;servlet-mapping&gt;
&lt;servlet-name&gt;myapp&lt;/servlet-name&gt;
&lt;url-pattern&gt;*.html&lt;/url-pattern&gt;
&lt;/servlet-mapping&gt;</source>
<span class="info">
<strong>Note:</strong>
<p>
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).
</p>
</span>
</subsection>
<subsection name="direct-service-encoder">
<p>
A specialized encoder used
<em>exclusively</em>
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.
</p>
<p>
A typical URL might be:
<code>/admin/Menu,border.link.direct</code>
. This indicates a page name of
<code>admin/Menu</code>
and a component id of
<code>border.link</code>
. By convention, the ".direct" extension is for stateless URLs.
</p>
<p>The hivemodule.xml contribution:</p>
<source xml:space="preserve">
&lt;contribution configuration-id="tapestry.url.ServiceEncoders"&gt;
&lt;direct-service-encoder id="direct" stateless-extension="direct" stateful-extension="sdirect"/&gt;
&lt;/contribution&gt;
</source>
<p>
In addition, the
<code>*.direct</code>
and
<code>*.sdirect</code>
mappings must be added to web.xml:
</p>
<source xml:space="preserve">
&lt;servlet-mapping&gt;
&lt;servlet-name&gt;myapp&lt;/servlet-name&gt;
&lt;url-pattern&gt;*.direct&lt;/url-pattern&gt;
&lt;/servlet-mapping&gt;
&lt;servlet-mapping&gt;
&lt;servlet-name&gt;myapp&lt;/servlet-name&gt;
&lt;url-pattern&gt;*.sdirect&lt;/url-pattern&gt;
&lt;/servlet-mapping&gt; </source>
</subsection>
<subsection name="asset-encoder">
<p>
The
<code>asset-encoder</code>
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.
</p>
<p>
In addition, each request includes a
<em>message digest</em>
, a string generated from the bytes of the the resource. This message digest
acts as a
<em>credential</em>
, 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.
</p>
<p>
To enable friendly URLs for the asset service, add the following to your
hivemodule.xml:
</p>
<source xml:space="preserve">
&lt;contribution configuration-id="tapestry.url.ServiceEncoders"&gt;
&lt;asset-encoder id="asset" path="/assets"/&gt;
&lt;/contribution&gt;</source>
<p>
This contribution will encode asset URLs using the given path. The provided
path,
<code>/assets</code>
comes first, then the digest string, then the path for the URL. An example
URI would be
<code>
/assets/91ab6d51232df0384663312f405babbe/org/apache/tapestry/contrib/palette/select_right.gif
</code>
.
</p>
<p>In addition you must add a mapping to web.xml:</p>
<source xml:space="preserve">
&lt;servlet-mapping&gt;
&lt;servlet-name&gt;myapp&lt;/servlet-name&gt;
&lt;url-pattern&gt;/assets/*&lt;/url-pattern&gt;
&lt;/servlet-mapping&gt;</source>
<p>
If you choose a different folder than
<code>/assets/</code>
then be sure to make corresponding changes in both hivemodule.xml and
web.xml.
</p>
</subsection>
<subsection name="extension-encoder">
<p>
The
<code>extension-encoder</code>
is used to encode just the
<code>service</code>
query parameter. The output URL is the service name with a fixed extension
(typically, ".svc"), i.e.,
<code>/home.svc</code>
or
<code>/restart.svc</code>
.
</p>
<p>In your hivemodule.xml:</p>
<source xml:space="preserve">
&lt;contribution configuration-id="tapestry.url.ServiceEncoders"&gt;
&lt;extension-encoder id="extension" extension="svc" after="*"/&gt;
&lt;/contribution&gt;</source>
<p>
The use of the
<code>after</code>
attribute ensures that this encoder is always executed after any other
encoders. Order is important!
</p>
<p>For this example, another mapping is required in the web.xml:</p>
<source xml:space="preserve">
&lt;servlet-mapping&gt;
&lt;servlet-name&gt;myapp&lt;/servlet-name&gt;
&lt;url-pattern&gt;*.svc&lt;/url-pattern&gt;
&lt;/servlet-mapping&gt;</source>
</subsection>
<subsection name="encoder">
<p>
Finally, when one of the pre-defined encoders is insufficient, you can
define your own. The &lt;encoder&gt; element allows an arbitrary object that
implements the
<a
href="../apidocs/org/apache/tapestry/engine/ServiceEncoder.html">
ServiceEncoder
</a>
interface to be plugged into the pipeline. The &lt;encoder&gt; element
supports the (required) id attribute, and the optional before and after
attributes.
</p>
<p>
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
<a href="../components/link/externallink.html">ExternalLink</a>
component for more information about using this engine service). The end
result is that the URLs for these two pages look like
<code>/vlib/book/2096</code>
rather than
<code>/vlib/ViewBook.external?sp=2096</code>
or
<code>/vlib/app?page=ViewBook&amp;service=external&amp;sp=2096</code>
. Certainly the first option is by far the prettiest.
</p>
<p>These encoders are configured in hivemodule.xml as follows:</p>
<source xml:space="preserve">
&lt;encoder id="viewbook" before="external" object="instance:ViewPageEncoder,pageName=ViewBook,url=/book"/&gt;
&lt;encoder id="viewperson" before="external" object="instance:ViewPageEncoder,pageName=ViewPerson,url=/person"/&gt;
&lt;page-service-encoder id="external" extension="external" service="external"/&gt;
</source>
<p>
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.
</p>
<p>The two special pages are mapped in web.xml using their custom URLs:</p>
<source xml:space="preserve"> &lt;servlet-mapping&gt;
&lt;servlet-name&gt;vlib&lt;/servlet-name&gt;
&lt;url-pattern&gt;/book/*&lt;/url-pattern&gt;
&lt;/servlet-mapping&gt;
&lt;servlet-mapping&gt;
&lt;servlet-name&gt;vlib&lt;/servlet-name&gt;
&lt;url-pattern&gt;/person/*&lt;/url-pattern&gt;
&lt;/servlet-mapping&gt;
</source>
<p>
The implementation of the ViewPageEncoder class is all about an encode()
method and a matching decode() method.
</p>
<p>
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
<a
href="../apidocs/org/apache/tapestry/engine/ServiceEncoding.html">
ServiceEncoding
</a>
:
</p>
<source xml:space="preserve">
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);
} </source>
<p>
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
</p>
<p>
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:
</p>
<source xml:space="preserve"> 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);
} </source>
</subsection>
<p>
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.
</p>
</subsection>
</section>
</body>
</document>