You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 18 Next »

Contents

Introduction

The Tapestry Cookbook is a collection of tips and tricks for commonly occurring patterns in Tapestry.

 

Many of the components provided with Tapestry share a common behavior: if the component's id matches a property of the container, then some parameter of the component (usually value) defaults to that property.

This is desirable, in terms of not having to specify the component's id and then specify the same value as some other parameter.

Let's say you have created a component, RichTextEditor, which operates like a normal TextArea component, but provides a JavaScript rich text editor. You might start with something like:

public class RichTextEditor implements Field
{
  @Property
  @Parameter(required=true)
  private String value;

  . . . // Lots more code not shown here
}

However, the weakness here is when you make use of the component. You template may look like:

    <t:label for="profile"/>
    <br/>
    <t:richtexteditor t:id="profile" value="profile"/>

Every component has a unique id; if you don't assign one with the t:id attribute, Tapestry will assign a less meaningful one. Component ids can end up inside URLs or used as query parameter names, so using meaningful ids helps if you are ever stuck debugging a request.

This repetition can be avoided by adding the autoconnect attribute to the @Parameter annotation:

  @Property
  @Parameter(required=true, autoconnect=true)
  private String value;

This can now be written as <t:richtexteditor t:id="profile"/>. The unwanted repetition is gone: we set the id of the component and the property it edits in a single pass.

If there is no matching property, then a runtime exception will be thrown when loading the page because the value parameter is required and not bound.

The most common case of using autoconnect is form control components such as TextField and friends ... or this RichTextEditor.

 

 

One of Tapestry's best features is its comprehensive exception reporting. The level of detail is impressive and useful.

Of course, one of the first questions anyone asks is "How do I turn it off?" This exception reporting is very helpful for developers but its easy to see it as terrifying for potential users. Catching runtime exceptions can be a very useful way of handling rarely occurring exceptions even in production, and there's no reason to throw away Tapestry's default error reporting just to handle a few specific exceptions. From version 5.4 (for previous versions, the same functionality is available as a third-party module tapestry-exceptionpage), you can contribute exception handles and/or exception pages for specific exception types. Refer back to Runtime Exceptions page for more information. Read on if you want to completely replace Tapestry's default exception handling.

Version 1: Replacing the Exception Report Page

Let's start with a page that fires an exception from an event handler method.

ActionFail.tml
 <html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_4.xsd" t:type="layout" title="Action Fail">
        <p>
            <t:actionlink t:id="fail" class="btn btn-large btn-warning">Click for Exception</t:actionlink>
        </p>
</html>
Index.java
package com.example.newapp.pages;

public class ActionFail {
    void onActionFromFail() {
        throw new RuntimeException("Failure inside action event handler.");
    }
}

With production mode disabled, clicking the link displays the default exception report page:

 

 

The easy way to override the exception report is to provide an ExceptionReport page that overrides the one provided with the framework.

This is as easy as providing a page named "ExceptionReport". It must implement the ExceptionReporter interface.

ExceptionReport.tml
<html t:type="layout" title="Exception"
      xmlns:t="http://tapestry.apache.org/schema/tapestry_5_4.xsd">


    <div class="panel panel-danger">
        <div class="panel-heading">An exception has occurred.</div>
        <div class="panel-body">
            ${message}
        </div>
        <div class="panel-footer">
            <t:pagelink page="index" class="btn btn-default">Home</t:pagelink>
        </div>

    </div>


</html>
ExceptionReport.java
package com.example.newapp.pages;

import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.services.ExceptionReporter;

public class ExceptionReport implements ExceptionReporter {

    @Property
    private String message;

    @Override
    public void reportException(Throwable exception) {
        message = exception.getMessage();

        if (message == null)
            message = exception.getClass().getName();
    }
}

The end result is a customized exception report page.

 

Version 2: Overriding the RequestExceptionHandler

The previous example will display a link back to the Index page of the application. Another alternative is to display the error <on> the Index page. This requires a different approach: overriding the service responsible for reporting request exceptions.

The service RequestExceptionHandler is responsible for this.

By replacing the default implementation of this service with our own implementation, we can take control over exactly what happens when a request exception occurs.

We'll do this in two steps. First, we'll extend the Index page to serve as an ExceptionReporter. Second, we'll override the default RequestExceptionHandler to use the Index page instead of the ExceptionReport page. Of course, this is just one approach.

Index.tml (partial)
 <t:if test="message">
    <div class="panel panel-danger">
        <div class="panel-heading">An exception has occurred.</div>
        <div class="panel-body">
            ${message}
        </div>
    </div>
 </t:if>
Index.java
public class Index implements ExceptionReporter
{
	@Property
	private String message;

	public void reportException(Throwable exception)
	{
   		message = exception.getMessage();

	   if (message == null) {
    	  message = exception.getClass().getName();
	   }
	}

  ...
}

The above defines a new property, message, on the Index page. The @Persist annotation indicates that values assigned to the field will persist from one request to another. The use of FLASH for the persistence strategy indicates that the value will be used until the next time the page renders, then the value will be discarded.

The message property is set from the thrown runtime exception.

The remaining changes take place inside AppModule.

AppModule.java (partial)
    public RequestExceptionHandler buildAppRequestExceptionHandler(
            final Logger logger,
            final ResponseRenderer renderer,
            final ComponentSource componentSource)
    {
        return new RequestExceptionHandler()
        {
            public void handleRequestException(Throwable exception) throws IOException
            {
                logger.error("Unexpected runtime exception: " + exception.getMessage(), exception);

                ExceptionReporter index = (ExceptionReporter) componentSource.getPage("Index");

                index.reportException(exception);

                renderer.renderPageMarkupResponse("Index");
            }
        };
    }

    public void contributeServiceOverride(
            MappedConfiguration<Class, Object> configuration,

            @Local
            RequestExceptionHandler handler)
    {
        configuration.add(RequestExceptionHandler.class, handler);
    }

First we define the new service using a service builder method. This is an alternative to the bind() method; we define the service, its interface type (the return type of the method) and the service id (the part that follows "build" is the method name) and provide the implementation inline. A service builder method must return the service implementation, here implemented as an inner class.

The Logger resource that is passed into the builder method is the Logger appropriate for the service. ResponseRenderer and ComponentSource are two services defined by Tapestry.

With this in place, there are now two different services that implement the RequestExceptionHandler interface: the default one built into Tapestry (whose service id is "RequestExceptionHandler") and the new one defined in this module, "AppRequestExceptionHandler"). Without a little more work, Tapestry will be unable to determine which one to use when an exception does occur.

Tapestry has a pipeline for resolving injected dependencies; the ServiceOverride service is one part of that pipeline. Contributions to it are used to override an existing service, when the injection is exclusively by type.

Here we inject the AppRequestExceptionHandler service and contribute it as the override for type RequestExceptionHandler. The @Local annotation is used to select the RequestHandler service defined by this module, AppModule. Once contributed into ServiceOverride, it becomes the default service injected throughout the Registry.

This finally brings us to the point where we can see the result:

 

Version 3: Decorating the RequestExceptionHandler

A third option is available: we don't define a new service, but instead decorate the existing RequestExceptionHandler service. This approach means we don't have to make a contribution to the ServiceOverride service.

Service decoration is a powerful facility of Tapestry that is generally used to "wrap" an existing service with an interceptor that provides new functionality such as logging, security, transaction management or other cross-cutting concerns. The interceptor is an object that implements the same interface as the service being decorated, and usually delegates method invocations to it.

However, there's no requirement that an interceptor for a service actually invoke methods on the service; here we contribute a new implementation that replaces the original:

AppModule.java (partial)
    public RequestExceptionHandler decorateRequestExceptionHandler(
            final Logger logger,
            final ResponseRenderer renderer,
            final ComponentSource componentSource,
            @Symbol(SymbolConstants.PRODUCTION_MODE)
            boolean productionMode,
            Object service)
    {
        if (!productionMode) return null;

        return new RequestExceptionHandler()
        {
            public void handleRequestException(Throwable exception) throws IOException
            {
                logger.error("Unexpected runtime exception: " + exception.getMessage(), exception);

                ExceptionReporter index = (ExceptionReporter) componentSource.getPage("Index");

                index.reportException(exception);

                renderer.renderPageMarkupResponse("Index");
            }
        };
    }

As with service builder methods and service configuration method, decorator methods are recognized by the "decorate" prefix on the method name. As used here, the rest of the method name is used to identify the service to be decorated (there are other options that allow a decorator to be applied to many different services).

A change in this version is that when in development mode (that is, when not in production mode) we use the normal implementation. Returning null from a service decoration method indicates that the decorator chooses not to decorate.

The Logger injected here is the Logger for the service being decorated, the default RequestExceptionHandler service.

Otherwise, we return an interceptor whose implementation is the same as the new service in version #2.

The end result is that in development mode we get the full exception report, and in production mode we get an abbreviated message on the application's Index page.

 

 Informal parameters are any additional parameters (aka HTML attributes) beyond the those explicitly defined for a component using the @Parameter annotation.

Any component that closely emulates a particular HTML element should support informal parameters, because it gives users of your component the ability to easily add HTML attributes to the HTML that your component emits. You'll find that most of the built-in Tapestry components, such as Form, Label and TextField, do exactly that.

To support informal parameters, a component class should use either the @SupportsInformalParameters annotation or the RenderInformals mixin. Otherwise, providing informal parameters to a component will do nothing: any additional parameters will be ignored.

Approach 1: @SupportsInformalParameters

In the example below we create an Img component, a custom replacement for the <img> tag. Its src parameter will be an asset. We'll use the @SupportsInformalParameters annotation to tell Tapestry that the component should support informal parameters.

Img.java
@SupportsInformalParameters
public class Img
{
    @Parameter(required=true, allowNull=false, defaultPrefix=BindingConstants.ASSET)
    private Asset src;

    @Inject
    private ComponentResources resources;

    boolean beginRender(MarkupWriter writer)
    {
         writer.element("img", "src", src);
         resources.renderInformalParameters(writer);
         writer.end();
         return false;
    }
}

The call to renderInformalParameters() is what converts and outputs the informal parameters. It should occur after your code has rendered attributes into the element (earlier written attributes will not be overwritten by later written attributes).

Returning false from beginRender() ensures that the body of the component is not rendered, which makes sense for an <img> tag, which has no body.

Approach 2: RenderInformals

Another, equivalent, approach is to use the RenderInformals mixin (:

Img.java
public class Img
{
    @Parameter(required=true, allowNull=false, defaultPrefix=BindingConstants.ASSET)
    private Asset src;

    @Mixin
    private RenderInformals renderInformals;

    void beginRender(MarkupWriter writer)
    {
        writer.element("img", "src", src);
    }

    boolean beforeRenderBody(MarkupWriter writer)
    {
        writer.end();
        return false;
    }
}

This variation splits the rendering of the tag in two pieces, so that the RenderInformals mixin can operate (after beginRender() and before beforeRenderBody()).

Approach 3: Extend the "Any" component

Another approach is to have your component class extend Tapestry's Any component, which already supports informal parameters:

Img.java
public class Img extends Any { ... }

 

 

 

 

 

This page has not yet been fully updated for Tapestry 5.4. Things are different and simpler in 5.4 than in previous releases.

Creating Component Libraries

Nearly every Tapestry application includes a least a couple of custom components, specific to the application. What's exciting about Tapestry is how easy it is to package components for reuse across many applications ... and the fact that applications using a component library need no special configuration.

Related Articles

A Tapestry component library consists of components (and optionally mixins, pages and component base classes). In addition, a component library will have a module that can define new services (needed by the components) or configure other services present in Tapestry. Finally, components can be packaged with assets: resources such as images, stylesheets and JavaScript libraries that need to be provided to the client web browser.

We're going to create a somewhat insipid component that displays a large happy face icon.

Tapestry doesn't mandate that you use any build system, but we'll assume for the moment that you are using Maven 2. In that case, you'll have a pom.xml file something like the following:

pom.xml
<project>
  <modelVersion>4.0.0</modelVersion>
  <groupId>org.example</groupId>
  <artifactId>happylib</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>
  <name>happylib Tapestry 5 Library</name>

  <dependencies>
    <dependency>
      <groupId>org.apache.tapestry</groupId>
      <artifactId>tapestry-core</artifactId>
      <version>${tapestry-release-version}</version>
    </dependency>

    <dependency>
      <groupId>org.testng</groupId>
      <artifactId>testng</artifactId>
      <version>5.1</version>
      <classifier>jdk15</classifier>
      <scope>test</scope>
    </dependency>
  </dependencies>

  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>1.5</source>
          <target>1.5</target>
          <optimize>true</optimize>
        </configuration>
      </plugin>

      <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-jar-plugin</artifactId>
           <configuration>
           <archive>
             <manifestEntries>
               <Tapestry-Module-Classes>org.example.happylib.services.HappyModule</Tapestry-Module-Classes>
             </manifestEntries>
           </archive>
           </configuration>
       </plugin>

    </plugins>
  </build>

  <repositories>
    <repository>
      <id>codehaus.snapshots</id>
      <url>http://snapshots.repository.codehaus.org</url>
    </repository>
    <repository>
      <id>OpenQA_Release</id>
      <name>OpenQA Release Repository</name>
      <url>http://archiva.openqa.org/repository/releases/</url>
    </repository>
  </repositories>

  <properties>
    <tapestry-release-version>5.4-beta-28</tapestry-release-version>
  </properties>
</project>

You will need to modify the Tapestry release version number ("5.2.0" in the listing above) to reflect the current version of Tapestry when you create your component library.

We'll go into more detail about the relevant portions of this POM in the later sections.

Step 1: Choose a base package name

Just as with Tapestry applications, Tapestry component libraries should have a unique base package name. In this example, we'll use org.examples.happylib.

As with an application, we'll follow the conventions: we'll place the module for this library inside the services package, and place pages and components under their respective packages.

Step 2: Create your pages and/or components

Our component is very simple:

HappyIcon.java
package org.example.happylib.components;

import org.apache.tapestry5.Asset;
import org.apache.tapestry5.MarkupWriter;
import org.apache.tapestry5.annotations.Path;
import org.apache.tapestry5.ioc.annotations.Inject;

public class HappyIcon
{
    @Inject
    @Path("happy.jpg")
    private Asset happyIcon;

    boolean beginRender(MarkupWriter writer)
    {
        writer.element("img", "src", happyIcon);
        writer.end();

        return false;
    }
}

HappyIcon appears inside the components sub-package. The happyIcon field is injected with the the Asset for the file happy.jpg. The path specified with the @Path annotation is relative to the HappyIcon.class file; it should be stored in the project under src/main/resources/org/example/happylib/components.

Tapestry ensures that the happy.jpg asset can be accessed from the client web browser; the src attribute of the <img> tag will be a URL that directly accesses the image file ... there's no need to unpackage the happy.jpg file. This works for any asset file stored under the library's root package.

This component renders out an <img> tag for the icon.

Often, a component library will have many different components, or even pages.

Step 3: Choose a virtual folder name

In Tapestry, components that have been packaged in a library are referenced using a virtual folder name. It's effectively as if the application had a new root-level folder containing the components.

In our example, we'll use "happy" as the folder name. That means the application can include the HappyIcon component in the template using any of the following, which are all equivalent:

  • <t:happy.happyicon/>
  • <t:happy.icon/>
  • <img t:type="happy.happyicon"/>
  • <img t:type="happy/icon"/>

Why "icon" vs. "happyicon"? Tapestry notices that the folder name, "happy" is a prefix or suffix of the class name ("HappyIcon") and creates an alias that strips off the prefix (or suffix). To Tapestry, they are completely identical: two different aliases for the same component class name.

The above naming is somewhat clumsy, and can be improved by introducing an additional namespace into the template:

<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"
  xmlns:h="tapestry-library:happy">

  ...

  <h:icon/>

  ...
</html>

The special namespace mapping for sets up namespace prefix "h:" to mean the same as "happy/". It then becomes possible to reference components within the happy virtual folder directly.

Step 4: Configure the virtual folder

Tapestry needs to know where to search for your component class. This is accomplished in your library's IoC module class, by making a contribution to the ComponentClassResolver service configuration.

At application startup, Tapestry will read the library module along with all other modules and configure the ComponentClassResolver service using information in the module:

HappyModule.java
package org.example.happylib.services;

import org.apache.tapestry5.ioc.Configuration;
import org.apache.tapestry5.services.LibraryMapping;

public class HappyModule
{
    public static void contributeComponentClassResolver(Configuration<LibraryMapping> configuration)
    {
        configuration.add(new LibraryMapping("happy", "org.example.happylib"));
    }
}

The ComponentClassResolver service is responsible for mapping libraries to packages; it takes as a contribution a collection of these LibraryMapping objects. Every module may make its own contribution to the ComponentClassResolver service, mapping its own package ("org.example.happylib") to its own folder ("happy").

This module class is also where you would define new services that can be accessed by your components (or other parts of the application).

It is possible to add a mapping for "core", the core library for Tapestry components; all the built-in Tapestry components (TextField, BeanEditForm, Grid, etc.) are actually in the core library. When Tapestry doesn't find a component in your application, it next searches inside the "core" library. Contributing an additional package as "core" simply extends the number of packages searched for core components (it doesn't replace Tapestry's default package, org.apache.tapestry5.corelib). Adding to "core" is sometimes reasonable, if you ensure that there is virtually no chance of a naming conflict (via different modules contributing packages to core with conflicting class names).

Step 5: Configure the module to autoload

For Tapestry to load your module at application startup, it is necessary to put an entry in the JAR manifest. This is taken care of in the pom.xml above:

pom.xml (partial)
      <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-jar-plugin</artifactId>
           <configuration>
           <archive>
             <manifestEntries>
             <Tapestry-Module-Classes>org.example.happylib.services.HappyModule</Tapestry-Module-Classes>
             </manifestEntries>
           </archive>
           </configuration>
       </plugin>

Step 6: Extending Client Access

As of Tapestry 5.2, a new step is needed: extending access for the assets. This is accomplished in your library's module class, HappyModule:

public static void contributeRegexAuthorizer(Configuration<String> configuration)
{
    configuration.add("^org/example/happylib/.*\\.jpg$");
}

This contribution uses a regular expression to identify that any resource on the classpath under the org/example/happylib folder with a jpg extension is allowed. If you had a mix of different image types, you could replace jpg with (jpg|gif|png).

Step 7: Versioning Assets

Classpath assets, those packaged in JAR files (such as the happy.jpg asset) are retrieved by the client web browser using a URL that reflects the package name. Tapestry users a special virtual folder, /assets, under the context folder for this purpose.

The image file here is exposed to the web browser via the URL /happyapp/assets/org/example/happylib/components/happy.jpg (this assumes that the application was deployed as happyapp.war).

Tapestry uses a far-future expiration date for classpath assets; this allows browsers to aggressively cache the file, but in Tapestry 5.1 and earlier this causes a problem when a later version of the library changes the file. This is discussed in detail in Yahoo's Performance Best Practices.

To handle this problem in Tapestry 5.1 and earlier, you should map your library assets to a versioned folder. This can be accomplished using another contribution from the HappyModule, this time to the ClasspathAssetAliasManager service whose configuration maps a virtual folder underneath /assets to a package:

public static void contributeClasspathAssetAliasManager(MappedConfiguration<String, String> configuration)
{
    configuration.add("happylib/1.0", "org/example/happylib");
}

With this in place, and the library and applications rebuilt and redeployed, the URL for happy.jpg becomes /happyapp/assets/happylib/1.0/components/happy.jpg. This is shorter, but also incorporates a version number ("1.0") that can be changed in a later release.

Added in 5.2

Conclusion

That's it! Autoloading plus the virtual folders for components and for assets takes care of all the issues related to components. Just build your JARs, setup the JAR Manifest, and drop them into your applications.

 

Switching Cases

With Tapestry's If component you can only test one condition at a time. In order to distinguish multiple cases, you'd have to write complex nested if/else constructs in your page template and have a checker method for each test inside your page class.

 

In cases where you have to distinguish multiple cases, the Delegate component comes in. It delegates rendering to some other component, for example a Block. For each case you have, you basically wrap the content inside a Block that doesn't get rendered by default. You then place a Delegate component on your page and point it to a method inside your page class that will decide which of your Blocks should be rendered.

Imagine for example a use case, where you want to distinguish between 4 cases and you have an int property called whichCase that should be tested against. Your page template would look as follows:

SwitchMe.tml
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_4.xsd">
    <body>
        <h1>Switch</h1>

        <t:delegate to="case"/>

        <t:block t:id="case1">
            Here is the content for case1.
        </t:block>

        <t:block t:id="case2">
            Here is the content for case2.
        </t:block>
        
        <t:block t:id="case3">
            Here is the content for case3.
        </t:block>
        
        <t:block t:id="case4">
            Here is the content for case4.
        </t:block>
    </body>
</html>

You can see, that the Delegate component's to parameter is bound to the case property of your page class. In your page class you therefore have a getCase() method that is responsible for telling the Delegate component which component should be rendered. For that we are injecting references to the Block}}s defined in your page template into the page class and return the according {{Block in the getCase() method.

SwitchMe.java
public class SwitchMe
{
    @Persist
    private int whichCase;

    @Inject
    private Block case1, case2, case3, case4;

    public Object getCase()
    {
        switch (whichCase)
        {
            case 1:
                return case1;
            case 2:
                return case2;
            case 3:
                return case3;
            case 4:
                return case4;
            default:
                return null;
        }
    }
}

Happy switching!

Enum Component Parameter

It's not uncommon to create a component that has a bit of complex behavior that you want to be able to easily control, and an enumerated type (a Java enum) seems like the right approach.

Our example comes from Tapestry's Select component, which has a blankOption parameter that has an enum type.

Let's start with the enum type itself:

BlankOption.java
public enum BlankOption
{
    /** Always include the blank option, even if the underlying property is required. */
    ALWAYS,

    /** Never include the blank option, even if the underlying property is optional. */
    NEVER,

    /** The default: include the blank option if the underlying property is optional. */
    AUTO;
}

Next, we define the parameter:

Select.java (partial)
    /**
     * Controls whether an additional blank option is provided. The blank option precedes all other options and is never
     * selected. The value for the blank option is always the empty string, the label may be the blank string; the
     * label is from the blankLabel parameter (and is often also the empty string).
     */
    @Parameter(value = "auto", defaultPrefix = BindingConstants.LITERAL)
    private BlankOption blankOption;

Note the use of literal as the default prefix; this allows us to use the name of the option in our template, e.g. <t:select blankoption="never" .../>. Without the default prefix setting, "never" would be interpreted as a property expression (and you'd see an error when you loaded the page).

The final piece of the puzzle is to inform Tapestry how to convert from a string, such as "never", to a BlankOption value.

TapestryModule.java (partial)
    public static void contributeTypeCoercer(Configuration<CoercionTuple> configuration)
    {
       . . .
       
       add(configuration, BlankOption.class);

       . . .
    }

    private static <T extends Enum> void add(Configuration<CoercionTuple> configuration, Class<T> enumType)
    {
        configuration.add(CoercionTuple.create(String.class, enumType, StringToEnumCoercion.create(enumType)));
    }

The TypeCoercer service is ultimately responsible for converting the string to a BlankOption, but we have to tell it how, by contributing an appropriate CoercionTuple. The CoercionTuple identifies the source and target types (String and BlankOption), and an object to perform the coercion (an instance of StringToEnumCoercion, via the create() static method).



Serving Tapestry Pages as Servlet Error Pages

Do you want to dress up your site and use a snazzy Tapestry page instead of the default 404 error page? Using modern servlet containers, this is a snap!

Simply upgrade your application web.xml to the 2.4 version (or newer), and make a couple of changes:

web.xml
<?xml version="1.0" encoding="UTF-8"?>

<web-app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
  version="2.4">

  <display-name>Cookbook</display-name>
  <context-param>
    <param-name>tapestry.app-package</param-name>
    <param-value>com.example.newapp</param-value>
  </context-param>

  <filter>
    <filter-name>app</filter-name>
    <!-- org.apache.tapestry5.TapestryFilter if not Tapestry 5.7.0+ -->
    <filter-class>org.apache.tapestry5.http.TapestryFilter</filter-class> 
  </filter>
  <filter-mapping>
    <filter-name>app</filter-name>
    <url-pattern>/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
    <dispatcher>ERROR</dispatcher>
  </filter-mapping>

  <error-page>
    <error-code>404</error-code>
    <location>/error404</location>
  </error-page>

</web-app>

Tapestry's filter must be marked as a handler for both standard requests and errors. That's accomplished with the <dispatcher> elements inside the <filter-mapping> section.

You must then map error codes to Tapestry URLs. In this case, the 404 error is send to the /error404 resource, which is really the "Error404" Tapestry page.

We'll create a simple Error404 page, one that displays a message and (in development mode) displays the details about the incoming request.

Error404.tml
<html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_4.xsd">
 <head>

        <title>Resource not found.</title>
    </head>
    <body>

        <div class="container">

            <h1>Page or resource not found.</h1>

            <t:if test="! productionMode">
                <t:renderobject object="request"/>
            </t:if>
        </div>

    </body>
</html>

The page simply makes the request and productionMode properties available:

Error404.java
package com.example.newapp.pages;

import org.apache.tapestry5.SymbolConstants;
import org.apache.tapestry5.annotations.Property;
import org.apache.tapestry5.ioc.annotations.Inject;
import org.apache.tapestry5.ioc.annotations.Symbol;
// org.apache.tapestry5.services.Request if not Tapestry 5.7.0+
import org.apache.tapestry5.services.http.Request; 

public class Error404
{
    @Property
    @Inject
    private Request request;

    @Property
    @Inject
    @Symbol(SymbolConstants.PRODUCTION_MODE)
    private boolean productionMode;
}


The end-result, in when not in production mode, looks like this:


An issue with an application that has a root Index page is that any invalid path, which would normally generate a 404 error, is instead routed to the Index page (because the invalid path looks like page's activation context). See Issue TAP5-2070.



Extending the If Component

The If component can be made very flexible; its main parameter, test, does not have to be bound to a boolean value, it merely has to be bound to a value that can be coerced to boolean.

For example, you may be working on an application that does a lot of Lucene searches, and you represent the results as a SearchResult object:

SearchResult.java
public class SearchResult<T> {
  public final Class<T> itemType;
  public final List<T> items;
  public final int size;
  public final int pages;
  public final int firstIndex;
  public final int lastIndex;

  public SearchResult(Class<T> type, List<T> items, int size, int pages, int firstIndex,
      int lastIndex) {
    this.itemType = type;
    this.items = items;
    this.size = size;
    this.pages = pages;
    this.firstIndex = firstIndex;
    this.lastIndex = lastIndex;
  }

  public boolean isEmpty() {
    return size == 0;
  }
}

In a SearchResult, the size property is the overall number of results from the search. The items list is a single "page" of those results to present to the user, consisting of items from firstIndex to lastIndex within the overall set.

In your templates, you have to check to see if the SearchResult exists, then see if it is empty, before you can get to the part that displays the content:

<t:if test="searchResult">
  <t:if test="! searchResult.empty">
    . . .
  </t:if>
</t:if>

The first test checks to see if searchResult is not null (null is treated as false). The second checks to see if the search result is empty.

What we'd like is for the test to look at the searchResult directly and treat an empty search result as false, and a non-empty search result as true. This is similar to what Tapestry already does for Collections.

This is just a matter of adding a Coercion:

AppModule (partial, Tapestry 5.7.0+)
public static void contributeTypeCoercer(MappedConfiguration<CoercionTuple.Key, CoercionTuple> configuration) {

  add(configuration, SearchResult.class, Boolean.class,
      new Coercion<SearchResult, Boolean>() {
        public Boolean coerce(SearchResult input) {
          return !input.isEmpty();
        }
      });
}

private static <S, T> void add(MappedConfiguration<CoercionTuple.Key, CoercionTuple> configuration,
    Class<S> sourceType, Class<T> targetType, Coercion<S, T> coercion) {
  CoercionTuple<S, T> tuple = new CoercionTuple<S, T>(sourceType,
      targetType, coercion);

  configuration.add(tuple.getKey(), tuple);
}


AppModule.java (partial, pre-Tapestry 5.7.0)
public static void contributeTypeCoercer(Configuration<CoercionTuple> configuration) {

  add(configuration, SearchResult.class, Boolean.class,
      new Coercion<SearchResult, Boolean>() {
        public Boolean coerce(SearchResult input) {
          return !input.isEmpty();
        }
      });
}

private static <S, T> void add(Configuration<CoercionTuple> configuration,
    Class<S> sourceType, Class<T> targetType, Coercion<S, T> coercion) {
  CoercionTuple<S, T> tuple = new CoercionTuple<S, T>(sourceType,
      targetType, coercion);

  configuration.add(tuple);
}

Inside this thicket of generics and brackets is the code that treats a SearchResult as a boolean: return !input.isEmpty();.

With this in place, the previous template can be simplified:

<t:if test="searchResult">
  . . .
</t:if>

The single test now implies that searchResult is not null and not empty.

  • No labels