CookbookPage edited by Howard M. Lewis ShipChanges (1)
Full ContentContents
IntroductionThe Tapestry Cookbook is a collection of tips and tricks for commonly occuring patterns in Tapestry. Default ParameterMany 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. Making this work involves two concepts: default parameter methods (methods that can compute a default value for a parameter), and a service, ComponentDefaultProvider. Let's say you have a component, OutputGadget, whose job is to output some information about an entity type, Gadget. public class OutputGadget { @Property @Parameter(required=true) private Gadget gadget; @Inject private ComponentDefaultProvider defaultProvider; @Inject private ComponentResources resources; Binding defaultGadget() { return defaultProvider.defaultBinding("gadget", resources); } } This can now be used as <t:outputgadget t:id="currentGadget"/>, assuming currentGadget is a property of the container. If there is no matching property, then the defaultGadget() method will return null, and a runtime exception will be thrown because the gadget parameter is required and not bound. The principal attribute on the Parameter annotation is not needed in the specific case; in some cases, a default for some other parameter may be based on the bound type of another parameter, the principal attribute forces the parameter to be resolved first. In many Tapestry form components, the value parameter is principal, so that the validate and translate parameters can computer defaults, based on the type and annotations bound to the value parameter. autoconnect attributeBecause this is such a common idiom, it has been made simpler for you. Rather than writing the code above, you can just use the autoconnect attribute of the Parameter annotation. This, effectively, creates the defaultGadget() method for you. In this case the code of component OutputGadget can be reduced to: public class OutputGadget { @Property @Parameter(required=true, autoconnect = true) private Gadget gadget; } Overriding Exception ReportingOne 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. Not that you'd have have runtime exceptions in production, of course, but even so ... Version 1: Replacing the Exception Report PageLet's start with a page that fires an exception from an event handler method. Index.tml <html xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"> <head> <title>Index</title> </head> <body> <p> <t:actionlink t:id="fail">click for exception</t:actionlink> </p> </body> </html> Index.java package com.example.tapestry2523.pages; public class Index { 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 xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"> <head> <title>Exception</title> </head> <body> <p>An exception has occurred: <strong>${exception.message}</strong> </p> <p> Click <t:pagelink page="index">here</t:pagelink> to restart. </p> </body> </html> ExceptionReport.java package com.example.tapestry2523.pages; import org.apache.tapestry5.annotations.Property; import org.apache.tapestry5.services.ExceptionReporter; public class ExceptionReport implements ExceptionReporter { @Property private Throwable exception; public void reportException(Throwable exception) { this.exception = exception; } } The end result is a customized exception report page. Version 2: Overriding the RequestExceptionHandlerThe 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">
<p>
An unexpected exception has occurred:
<strong>${message}</strong>
</p>
</t:if>
Index.java public class Index implements ExceptionReporter { @Property @Persist(PersistenceConstants.FLASH) private String message; public void reportException(Throwable exception) { message = exception.getMessage(); } void onActionFromFail() { throw new RuntimeException("Failure inside action event handler."); } } 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 an 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. This finally brings us to the point where we can see the result: Version 3: Decorating the RequestExceptionHandlerA 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 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. Supporting Informal ParametersInformal parameters are additional parameters beyond the formal parameters defined for a component using the Parameter annotation. A component that closely emulates a particular HTML element should also support informal parameters. You'll find that many of the built-in Tapestry components, such as Form, Label and TextField, do exactly that. Normally, specifying additional parameters for a component, beyond its formal parameters, does nothing: the additional parameters are ignored. The SupportsInformalParameters annotation is used to identify a component for which informal parameters are to be kept. The example is an Img component, a replacement for the <img> tag. Its src parameter will be an asset. @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 a <img> tag, which has no body. Another option is to use the RenderInformals mixin: 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()). Creating Component LibrariesNearly 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. A Tapestry component library consists of components (as well as component base class, pages and mixins). 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.2.0</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 nameJust 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 3: Create your pages and/or componentsOur 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 librarie's root package. This component renders out an img tag for the icon. Typically, a component library will have many different components and/or mixins, and may even provide pages. Step 2: Choose a virtual folder nameIn 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 will include the HappyIcon component in the template as:
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 3: Configure the libraryTapestry 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).
Step 4: Configure the module to autoloadFor 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>
ConclusionThat'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. A note about AssetsTapestry automatically creates a mapping for assets inside your JAR. In the above example, the icon image will be exposed as /assets/application version/happy/components/happy.jpg (the application version number is incorporated into the URL). The "happy" portion is a virtual folder that maps to the librarie's root package (as folder org/example/happylib on the Java classpath). The application version is a configurable value. In Tapestry 5.1 and earlier, it was necessary to explicitly create a mapping, via a contribution to the ClasspathAssetAliasManager service, to expose library assets. This is no longer necessary in Tapestry 5.2. Switching CasesWith 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_1_0.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! Unable to render {include} Couldn't find a page to include called: Enum Parameter Recipe
Change Notification Preferences
View Online
|
View Changes
|
- [CONF] Apache Tapestry > Cookbook confluence
- [CONF] Apache Tapestry > Cookbook confluence
- [CONF] Apache Tapestry > Cookbook confluence
