Introduction
Tapestry is a Java based web framework. It is a component oriented framework. You may use existing components to build pages but it is very easy to extend existing or build your own set of components to achieve a consistent look and feel in enterprise applications. Tapestry provides powerful form components which speed up the development process and it has a lot of functionality included to build Ajax enabled dialogs.
We have tested version 5.1 which is published under the Apache License Version 2. Tapestry 5 is incompatible with version 4. It was rebuild from scratch with the intention to provide a stable API for the future. There is a medium release step (5.0 to 5.1 etc) about once per year and a minor release (5.1.1 to 5.1.2) every 1-2 months.
The test took place in the summer 2009.
Website: http://tapestry.apache.org/
Hello world
The hello world application intends to show how complex it is to setup a simple application. It renders an index page with a link. Following a link calls an action of the framework and stores a localized message. The user is send to a view displaying this message.
You can download the sample project helloworld at
http://www.laliluna.de/download/framework-evaluation-samples.zip
Required steps
There is an option to use a maven archetype to build a sample application. (See http://tapestry.apache.org/tapestry5.1/tutorial1/ )
I will not use this option but setup the application manually.
Create a new web application.
Copy the following libraries from the Tapestry download to the WEB-INF/lib folder
- antlr-runtime-3.1.1.jar
- commons-codec-1.3.jar
- javassist-3.9.0.GA.jar
- log4j-1.2.14.jar
- slf4j-api-1.5.2.jar
- slf4j-log4j12-1.5.2.jar
- stax-api-1.0.1.jar
- stax2-api-3.0.1.jar
- tapestry-core-5.1.0.5.jar
- tapestry-ioc-5.1.0.5.jar
- tapestry5-annotations-5.1.0.5.jar
- woodstox-core-asl-4.0.3.jar
Modify the web.xml
Tapestry is integrated with a servlet filter. Inside of the web.xml there is another important configuration. It is the context param tapestry.app-package, which defines the root package of the application.
Tapestry will configure your application using a class named
${tapestry.app-package}/services/${filterName}Module.class
In our case this is de.laliluna.example.services.AppModule
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> <context-param> <param-name>tapestry.app-package</param-name> <param-value>de.laliluna.example</param-value> </context-param> <filter> <filter-name>app</filter-name> <filter-class>org.apache.tapestry5.TapestryFilter</filter-class> </filter> <filter-mapping> <filter-name>app</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app>
The application module is optional but we will use it to set a default locale and the development mode.
Tapestry distinguishes a production and a development mode. In production mode JavaScript are merged and compressed into single files.
In development mode pages, components and resource files are reloaded automatically, if you change them. In addition, you will get very detailed error messages.
An AppModule class
package de.laliluna.example.services; public class AppModule { public static void contributeApplicationDefaults( MappedConfiguration<String, String> configuration) { configuration.add(SymbolConstants.SUPPORTED_LOCALES, "en"); configuration.add(SymbolConstants.PRODUCTION_MODE, "false"); } }
Every page requires a page class and a corresponding template file.
We will create the HelloWorld page first. The class file has a property message. The message is set in the initialize method. This method will later be called by the Index page.
Tapestry use the Post-Redirect-Get pattern for all form submits and action requests.
This is a best practice as explained in the webframework evaluation article. As a consequence, we need to tell Tapestry which property should survive the redirect. This happens with the annotation @Persist
package de.laliluna.example.pages; // … imports public class HelloWorld { @Persist(org.apache.tapestry5.PersistenceConstants.FLASH) private String message; public String getMessage() { return message; } Object initialize(String message){ this.message = message; return this; } }
The template will access the getMessage method to display the message.
WebContent/HelloWorld.tml
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"> <head> <meta http-equiv="content-type" content="text/html; charset=utf-8"/> <title>Hello World</title> </head> <body> <div class="main"> ${message} </div> </body> </html>
Here is the index template in WebContent/Index.tml. The Index page has an action link. The method onAction is called in the corresponding page class, if the link is clicked.
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"> <head> <meta http-equiv="content-type" content="text/html; charset=utf-8"/> <title>Index</title> </head> <body> <div class="main"> <t:actionlink>Start</t:actionlink> </div> </body> </html>
The Index page uses two annotations to inject resources into the page class. @InjectPage will inject an instance of the HelloWorld page.
@Inject will inject the messages of the resource bundle. The method onAction retrieves a message from the resource bundle, passes it to the HelloWorld page and returns the page. If a page instance is returned from an action method, Tapestry will navigate to this page.
package de.laliluna.example.pages; public class Index { @InjectPage private HelloWorld helloWorld; @Inject private Messages messages; Object onAction() { String theMessage = messages.get("helloworld"); helloWorld.initialize(theMessage); return helloWorld; } }
Create a resource file
Create a app.properties file in the WEB-INF folder
greeting=Welcome to the sample application
helloworld=Hello world from the Tapestry application!
That’s it.
2. Discussion
Architecture
Framework concepts
Tapestry is a component oriented framework. A page which is a kind of component as well, consist of other components like input components, grids and loops. You can use existing components, extend existing components or use components provided by other people. There are component collections available for Tapestry.
Tapestry abstracts from the underlying technology and let the user work with page classes, components and events. Inside of Tapestry, there is an IOC container (Inversion of Control). Converting of fields, type coercion, validation, request chains and all the other functionality of a webframework are implemented as separated elements which are weaved together with this IOC container. The same container can be used to add application specific configurations and services. This concept has a great advantage, you can easily extend or replace Tapestry’s functionality. Just change or add elements to the IOC container and Tapestry will automatically pick it up. You will find some examples to adapt Tapestry to your needs in this article. You may have a look at the source code of the class org.apache.tapestry5.services.TapestryModule to get a first impression.
The configuration doesn’t have to take place in a single configuration class. You can provide additional Jars with a separate configuration class which adds components or elements to the central configuration. This kind of distributed configuration is useful for larger application. The idea was picked up from Eclipse.
A core element of an application is a page. A page has a page class and a page template. The simplest page class is just an empty class in the pages package.
package de.laliluna.example.pages; public class Sample { }
A simple Tapestry template – Sample.tml
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"> <head> <meta http-equiv="content-type" content="text/html; charset=utf-8"/> <title>A sample page</title> </head> <body> Sample page </body> </html>
A page class can provide data for the template.
public class Sample { public String getName(){ return "Sebastian"; } }
The template can use this data. ${name} will call the getName() method.
<body> Name: ${name} </body>
A page consist of components. Component can use data provided by the page as well. Furthermore a component can push data back to the page class instance. The next example uses a loop component which reads data from the page class and writes back the current value of the loop.
Here is the modified page class.
public class Sample { @Property private String country; public List<String> getCountries() { return Arrays.asList("France", "UK", "Netherland"); } }
The following Loop component displays the countries of the Sample page. It executes the getCountries method on the page class to read the countries. While looping it writes back its current value to the country property of the class. The expression ${country} reads this property and displays it.
<body> <t:loop source="countries" value="country"> Country: ${country}<br/> </t:loop> </body>
Page templates and page classes are tightly coupled and can interact with each other. A page makes use of components. The t:loop added a loop component to the page. Components follow the same concepts as pages. There is a component class and optionally a component template. Tapestry provides all the components you need to build a typical web application, you can find a list on http://tapestry.apache.org/tapestry5.1/tapestry-core/ref/.
You can extend them or add their behavior using so called mixins. You may use component collection like http://www.chenillekit.org/ or event build your own components. Most developers have never build their own components as it was a badly documented and not very simple tag to do this for JSP or JavaServer Faces. In Tapestry this is a lot easier. You will find examples in the later chapters of this article.
The communication with your page class happens with events. Tapestry creates a number of events when converting input, validating input, submitting forms or calling action methods. The page class can implement a corresponding event method to deal with the event.
public class Foo{ Object onAction() { …} Object onSuccess() {} // form successfully submitted void onValidateForm() throws ValidationException {} // validation }
Extension points
Basically, you can replace, extend or add functionality in all areas of Tapestry. I explained already that the use of an IOC container internally, makes Tapestry open for adaption. This is an architecture build for extension. You will find an example to add a request handler in the request filter chain in the chapter speaking about security.
The input validation examples explain how to build you own validators and integrate them into the application. In the how to chapter there are basic examples explaining how to write your own components. The double submit chapter shows how to create a mixin. A mixin is a possibility to add behavior to existing components. Once a mixin is created you can easily add it to existing components.
Here is a snippet adding a doublesubmit mixin to a normal form component.
<t:form t:mixins="doublesubmit">
Tapestry makes use of mixins as well. The Ajax autocomplete functionality of text fields is implemented as mixin.
<t:textField t:id="country" t:mixins="autocomplete" />
The extensibility is one of the greatest advantage of Tapestry.
How to integrate a business layer?
The simplest approach to integrate a business layer is to use the Tapestry Inversion of Control (IOC) container. IOC container like Guice, Spring, Pico or the Tapestry container are a nice way to build and configure your business layer.
Inside of the AppModule class, which configures your application, you can provide a bind method, which registers all of your services.
public static void bind(ServiceBinder binder) { binder.bind(AddressService.class, AddressServiceImpl.class); binder.bind(ArticleService.class, ArticleServiceImpl.class); }
In a page class the bound services can be injected with the help of the @Inject annotation.
Sample class CreateAddress makes use of the AddressService
public class CreateAddress { @Property private Address address; @Inject private AddressService addressService; Object onSuccess(){ addressService.create(address); return null; } }
If you want to use other technologies to implement your business layer, we need to follow a different approach. You might consider to build your business logic with EJB 3, Spring or the elegant Pico container for example.
In that case, we could provide a class which knows how to look up services of our container. For EJB application such a class is normally called ServiceLocator but we could name it ServiceFactory class as well.
This class can be integrated into the Tapestry as a normal service.
Extract of AppModule class
public static void bind(ServiceBinder binder) { binder.bind(ServiceLocator.class, ServiceLocatorImp.class); ...
In a page, the ServiceLocator class is injected and used to fetch the required service.
@Inject private ServiceLocator locator; public Object onSuccess(){ ArticleService articleService = locator.getService(); articleService.create(article); return SuccessPage.class }
If we compare this solution to the last one, we can see that there is always one more step. First we inject the ServiceLocator class, then we ask it to get the service. For Spring, there is a module tapestry-ioc, which removes this intermediate step. I would like to show you, how you can achieve this for other technologies as well.
Im going to use the PicoContainer as I like its powerful constructor injection. We are going to create a PicoContainer. The class needs to implement the Tapestry ObjectProvider interface. The Pico container has exactly one service: SamplePicoServiceImpl
public class PicoObjectProvider implements ObjectProvider { private static MutablePicoContainer container = new DefaultPicoContainer(); private final Logger logger = LoggerFactory.getLogger(PicoObjectProvider.class); public PicoObjectProvider() { // adding services to the container (=binding) container.addComponent(SamplePicoServiceImpl.class); } public <T> T provide(Class<T> objectType, AnnotationProvider annotationProvider, ObjectLocator locator) { T object = container.getComponent(objectType); if (object != null) logger.debug("Pico created object {}", objectType); return object; } }
Now, we add the object provider to the list of existing object providers. Here is an extract of the AppModule class
public static void contributeMasterObjectProvider(OrderedConfiguration<ObjectProvider> configuration){ configuration.add("PicoObjectProvider", new PicoObjectProvider(), "after:*"); }
Tapestry will now use this provider when looking for services to inject.
Usage sample
@Inject private SamplePicoService picoService; public Object onAction(){ picoService.hello(); return null; }
Let’s have a look at EJB technology integration. The approach used in the last example will cache the service inside the page. This is not be the best choice if you want to integrate EJB 3 based services. In a EJB container Stateless Session beans are only used by a thread and returned afterwards. Stateful session beans need to be stored in the HTTP session in order to use the same instance for a later request. Stateful session beans are probably best used with the ServiceLocator approach. They tend to require manual control, when to be stored or released.
For stateless session beans, we could follow the approach used by the Tapestry IOC container. It injects a proxy, which on access calls a JustInTimeObjectCreator to build the service. For services, annotated with @Scope(ScopeConstants.PERTHREAD), the creator delegates to a PerThreadServiceCreator which will create the service only on the first call in a thread, stores it into a ThreadLocal and registers a clean up listener to remove the service from the ThreadLocal after the request was processed.
We could either mirror this approach or create a proxy, which is able to do a JNDI lookup for the EJB 3 service and to bind this proxy using the PERTHREAD scope in the normal Tapestry IOC module.
Tapestry provides a Hibernate module which provides a nice integration of Hibernate into the application.
Dependencies and libraries
There is a reasonable number of dependencies. You can find the list at the beginning of the HelloWorld example. What I appreciate, that Tapestry does not use the commons* approach of other frameworks.
Flow of development
To create a new dialog, a page class and a page template has to be created. As new page classes are not detected by the reloading mechanism you have to restart the application. Once the files are there, it is only required to compile and press reload in the browser.
Popularity and contributor distribution
Google pages on tapestry framework: > 960,000
Books on Tapestry 5: 2
Number of mails on mailing list per month: 750
Number of core developer: 8
Number of regular patcher/contributors: 34 (est.)
My impression of the framework
My first impression was mediocre. The documentation is not good enough for beginners and a lot of caveats like the special directory structure of a Tapestry application is not detailed enough. The Quick start tutorial enforces Maven usage and generates a lot of files and directory. Building the first application without this was quite hard.
Once I overcame the first hurdles, I became more and more impressed. Building CRUD (create, read, update, delete) dialogs is incredible fast. The form component renders a form for a model, adding labels, input fields and validations. All this information is extracted from the model and its annotation and you don’t have to write a single line of code. Here is the code for a complete form.
<t:beaneditform object="person"/>
You have control over the generated form and the possibility to change whatever you need either application wide or just in a single form. As a consequence, you get even less code than in a Ruby on Rails application. The learning curve is of course steeper than the one of the Stripes framework, but this is naturally. Stripes is a thin layer above the underlying technologies. Tapestry abstracts from the underlying technology in order to provide a lot of powerful functionality.
After having explored the functionality of the framework, writing my own components, writing mixins to extend existing components, I came to the conclusion that Tapestry is one of the most innovative frameworks and probably even the best candidate for enterprise applications.
In a large enterprise application you have to provide a standardized look and feel throughout the application. You need to be able to extend existing components to let them perfectly integrate into your application style guide. The component concept of Tapestry is exactly made for this.
The Ajax support is advanced and at the same time simple to use. Tapestry is ready to be used for modern applications. It uses the Prototype and Scriptaculous JavaScript libraries. Both are robust and provide a lot of functionality.
To summarize, Tapestry provides a lot of useful functionality and a consequence requires more time to learn it as compared to lightweight frameworks. The documentation is not perfect and makes starting harder as required. Tapestry is a very good choice, if you look for an alternative to JavaServer Faces. If you look for a successor of Struts 1 and want to have something which follows Struts like concept, then Tapestry might not be your first choice.
The community’s opinion – beautiful features and concepts
I asked the following question on the mailing list:
What is your favorite feature or concept of Tapestry, what is unique as compared to other things, what is an eye catcher?
Here are extracts or rephrased parts of the feedbacks:
Thiago H. de Paula Figueiredo
There are many, but live class reloading (big productivity boost) and the very small amount of code needed to write almost anything in Tapestry are maybe the biggest one. Another one is that Tapestry is one of the few frameworks (JSF is not one of them) that treats pages as real objects, with behavior and state.
Peter Stavrinides
Tapestry IoC, (edit: Tapestry Inversion of Control container used internally to assemble the Tapestry webframework) which is without a doubt a masterfully written piece of software, IoC is of course not unique to Tapestry, but the intuitiveness of the container, the ease of configuration, and the freedom from XML are surely worth a mention
Mario Rabe
I like the amount of predictable magic the framework does. Thats what enables you writing much less code compared to others. You write the parts of your web-app that differ from the “common case” and T5 clues them together working app with some best-practices implemented for free. Howard calls this magic “adaptive API”, and thats in fact by far the greatest invention I’ve seen last years.
Inge Solvoll
My favorite part is that I always seem to get it right the first time around. In JSP/Struts, it pretty much always crashes (with extremely non-intuitive messages) when testing my code for the first time. In T5, I often end up writing code containing concepts and components I’m not 100% familiar with, just trying to do what makes sense to me, and most of the time it just works. And if it doesn’t, I get a message on the screen telling me how I can get it right by spending 8 more seconds :)
Igor Drobiazko
One of the most important things for me is that using Tapestry I can override or replace every piece of the framework. This fact makes Tapestry the most powerful Java web framework. After working with Tapestry for a while you will recognize that JavaServer Faces technology (for example) is a bloated and ugly monster.
Kristian Marinkovic
Component development: it is so easy to create components even complex ones. JSF needs at least 5-6 artifacts (java, xml, jsp) to create a component. T5 apps tend to have more component reuse and a reduced amount of duplicate gui code (jsf 2.0 is getting better here as well)
It is easy to learn: I trained our developers (from novice to experienced java developers) and it took them just about 2-3 days to become productive.
Paul Field
In addition to various cool things mentioned by other people, I really like the ability to unit test components and pages: http://tapestry.apache.org/tapestry5.1/guide/unit-testing-pages.html
It’s really nice to be able to do test-driven development in the GUI layer. (Particularly in conjunction with the tapestry-xpath project: http://tapestry.formos.com/nightly/tapestry-xpath/ ).
Howard Lewis Ship
Live Class Reloading
Can’t live without it. Encourages an exploratory style that’s very agile.
Negatives: everyone wants it for non-component classes. ClassLoader
issues passing components “outside the box”. Doesn’t pick up new files
perfectly, just changed.
Templates
Tapestry templates are simple and concise. They are readable. Their
structure on the server is very close to the final output on the client. It’s very easy to move back and
forth, and you don’t have the kind of visual impedance and clutter that you see in JSPs and other templating systems.
Negatives: A couple too many ways to do things (three different ways for an element to be a component). XML causes grief if including HTML entities without a DOCTYPE. A few minor ambiguities (mostly about doctypes and namespaces). Should never have allowed page templates in the context folder.
Rendering
Tapestry isn’t about rendering character data; rendering at all steps is a process by which objects (ultimately parsed from XML templates) render as … objects (the DOM) and it is the DOM that’s rendered as a character stream. Post-processing of the DOM simplifies everything (Tapestry 5 doesn’t need Shell and Body components, like T4). Rendering is not tail recursive and its very easy to manipulate the render order to accomplish very dynamic effects within Tapestry’s static structure dictum.
Negatives: more memory intensive, and slower to first-byte (on the client).
Naming Conventions, Annotations, Parameters
Everything works together so that Tapestry can “do the magic”. Code is concise, Tapestry fills in around the edges. Very smart defaults (that is, active code defaults rather that simply declarative).
Negatives: Many are more comfortable extending classes or implementing interfaces. Documentation gets tricky with the wealth of options and approaches. Some annotations have odd names. Annotations should have been divided up more clearly to indicate which apply to POJOs and which are only useful with components.
Performance
Tapestry is really fast! Especially in 5.1 where some effort was made to speed it up and streamline it. Lots of room for caching and optimizations.
Negatives: JSPs can be faster sometimes.
Service Configurations and Module Autoloading
Allows Tapestry to be extended just by “dropping in a JAR”. Modules are loaded, services provided, contributions contributed, everything just wires itself together.
Negatives: Modules can only be loaded at startup. Configurations can be tricky to master. Lots of modules makes it harder to understand how everything fits together.
TypeCoercer
Magic that lets data flow from component to component, efficiently and without regards to type. For example, lets you pass a List to a Grid component, even though the source parameter is type GridDataSource … the List is coerced. And its extensible, central to configuration, quite efficient.
Negatives: It’s so central that naive contributions (to extend it) too often result in Tapestry IoC startup failures that are hard to diagnose.
Friendly URLs
Tapestry built-in URL generation creates sensible rest-like URLs.
Negatives: People want absolute control over URLs; 5.1 provides this, but the jury is still out.
Public vs. Internal
T5 does what prior Tapestry did wrong: clear separation between internal code (subject to change at any time) and public APIs.
Negatives: people still need to use internals sometimes and face difficulties moving up from 5.0 to 5.1.
Extensibility
Every aspect of Tapestry can be overridden, extended, decorated, advised, improved, replaced, etc. The term “seam” has come into vogue as representing a location where code can be extended … Tapestry is pretty much nothing but seams.
Negatives: Almost two hundred services and all those configurations and contributions make it hard to find where to apply a change to reach a desired effect.
3. Features
Render / page technologies
Tapestry provides its own mechanism to render pages. It is called Tapestry Markup Language – TML.
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"> <head> <meta http-equiv="content-type" content="text/html; charset=utf-8"/> <title>${title}</title> </head> <body> <div class="main"> <h1>${title}</h1> <ul> <li><t:actionlink >Go to <i>Hello world!</i></t:actionlink></li> </ul> </div> </body> </html>
TML is XHTML or HTML enriched with Tapestry tags. The rendering mechanism is powerful and tags to indicate zones to allow partial updates are already included. This allows very efficient rendering when using Ajax. Custom tags to render user component can easily be created and used.
Developer comfort
In development mode, Tapestry reloads resource bundles, components and pages automatically. This improves development performance and provides you nearly with the same comfort as scripting languages like PHP, Ruby etc. You only have to restart your application if you create a new page.
Ajax support
Tapestry provides a number of functionalities to support Ajax enabled web application. You will find the complete code for the following examples in the howto project.
Tapestry Ajax support is based on Prototype and Scriptaculous. Both are rock solid JavaScript libraries. You can add a include a JavaScript file in a page using an annotation.
@IncludeJavaScriptLibrary("/foo/dragdrop.js") public class MyPage {
Tapestry will include not only the JavaScript as link in the page but – in production mode – also merge all JavaScript files into a single one. This is a best practice for fast websites.
Standard components like text input can be enriched with an autocomplete mixin.
<t:form> <t:label for="country"/> <t:textField t:id="country" t:mixins="autocomplete" /> <br/> <input type="submit" value="Continue"/> </t:form>
If you type in the text field, an Ajax call to the method onProvideCompletionsFromCountry in the corresponding page class is made. The method can simply return a list of Strings.
List<String> onProvideCompletionsFromCountry(String partial) { return countryService.getCountryNamesByPartial(partial); }
There are more mixins available and you can create your own mixins as well.
Another useful feature are the zones. A zone is an area of the template which can be filled with the content of an Ajax request. The following link will execute an Ajax request to the onActionFromFoo method of the page class and fill the div with the response.
<t:actionlink t:id="foo" zone="help">Fill the help zone with foo </t:actionlink> <div t:type="zone" t:id="help"></div>
Here is an extract of the page class.
Object onActionFromFoo(){ return new JSONObject().put("content", "Foo was called"); }
Alternatively you can use a block. A block is part of the Tapestry template which is normally not rendered. You can ask Tapestry to inject this block into the page class and return it for example from an action method.
Extract of the template file
<t:actionlink t:id="bar" zone="help">Fill the help zone with bar </t:actionlink> <div t:type="zone" t:id="help"></div> <t:block id="barBlock"> <div>This is the bar block</div> </t:block>
This time the action link sends an Ajax request calling the onActionFromBar method, which returns the block, which was injected by Tapestry into the class. Tapestry will render the content of the block inside of the target zone
@Inject private Block barBlock; Object onActionFromBar(){ return barBlock; }
To summarize, we define HTML snippets in the template, asks Tapestry to inject them into the page class and return them as response to an Ajax request.
The same works as well for multiple zones.
Our page has a help zone with user information and a zone rendering a form.
<t:actionlink t:id="update" zone="help">Start selecting continents </t:actionlink> The help zone <div t:type="zone" t:id="help"></div> The form zone <div t:type="zone" t:id="form"></div> <t:block id="helpBlock"> <div class="help">${helpMessage}</div> </t:block> <t:block id="continentBlock"> <t:form t:id="continentForm" zone="help"> <t:label for="continent"/> : <t:select t:id="continent" model="continents"/> <br/> <input type="submit" value="Continue"/> </t:form> </t:block>
Once we click on the action link, the help zone and the form zone are rerendered.
@Inject private Block helpBlock, continentBlock, countryBlock; Object onActionFromUpdate() { helpMessage = "Please select a continent"; return new MultiZoneUpdate("help", helpBlock).add("form", continentBlock); }
You will find more Ajax related feature like Streaming or support for JSON responses.
Finally you can easily create your own Ajax supporting components or use an existing component collection. In my opinion, the Ajax support is pretty complete and should enable you to build Ajax enabled web applications with Tapestry.
Security
There is no build in security but you will find a number of examples how to integrate Spring Security in the Tapestry wiki documentation.
But you are a software developer and actually security is something pretty easy to do. Here are my notes from my sample security module. It provides method based security. The method protectedMethod below can only be accessed by user having READ and ADMIN rights.
public interface SampleService { String allowed(); @Protected(rights = "READ,ADMIN") String protectedMethod(); }
In addition, I created a component to be used in templates. It shows a part of the page only, if the user has the required rights.
<t:hasRight right="READ"> This text is only shown if the user has the right READ </t:hasRight>
For the security models, I prefer the following approach. There is a user which might have a number of roles. Every role has a number of rights. Rights are the things that are hard coded in your source code. User and roles can be administrated.
We need a request filter which at the beginning of the request, reads a user from the HTTP session and stores it into a thread local for later use. At the end of the request it cleans up the ThreadLocal storage and puts the user back to the HTTP session or logs out the user if required. The following methods have to be added to the AppModule of the Tapestry application to integrate the request filter.
public RequestFilter buildSecurityFilter( final SecurityService securityService) { return new SecurityRequestFilter(securityService); } public void contributeRequestHandler( OrderedConfiguration<RequestFilter> configuration, @Local RequestFilter filter) { configuration.add("Security", filter); }
The security service – see implementation in the sample code – provides all the methods we need to work with the user.
public interface SecurityService { void storeUser(ApplicationUser user); void logoutUser(); void cleanStorage(); ApplicationUser getUser();
The ApplicationUser is a simple interface to be implemented by your user model.
public interface ApplicationUser { String getUsername(); boolean hasRight(String right); }
In order to protect the methods, we need an annotation and a method advise (alias interceptor or decorator), which is wired by the Tapestry IOC container.
@Protected annotation @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Protected { String rights(); }
The source code of the method advise is to long to show it here. Check the provided source code.
Then we tell Tapestry to wire the advice around all methods having the Protected
annotation of classes ending with Service by adding the following code to the AppModule class.
@Match("*Service") public static void adviseNonNull(MethodAdviceReceiver receiver, SecurityService securityService) { if (receiver.getInterface().equals(SecurityService.class)) return; for (Method m : receiver.getInterface().getMethods()) { Protected annotation = m.getAnnotation(Protected.class); if (annotation != null && annotation.rights() != null) { MethodSecurityAdvice advice = new MethodSecurityAdvice(securityService, annotation.rights()); receiver.adviseMethod(m, advice); } } }
If a method is marked with the annotation Protected
, Tapestry will call the advise method. The following code is extracted from the class MethodSecurityAdvice
and checks if the user has the required rights.
public void advise(Invocation invocation) { ApplicationUser user = securityService.getUser(); boolean hasRight = false; if (user != null) { for (String right : rights) { if (user.hasRight(right)) { hasRight = true; break; } } } if (hasRight) invocation.proceed(); else throw new NotAuthorizedException("You are not allowed to access " + invocation.getMethodName()); }
That’s all we need for method security. The last step is to write the components to allow checking of user rights in the template. Here is the complete component code rendering the content if the user has the required rights. Component writing is very simple with Tapestry.
public class HasRight { @Parameter(required = true, defaultPrefix = "literal") private String right; @Inject private SecurityService securityService; @BeginRender boolean checkAccess() { ApplicationUser user = securityService.getUser(); return (user != null && user.hasRight(right)); } }
Don’t forget that components have to be placed in the components package.
And here again the usage example:
<t:hasRight right="READ"> This text is only shown if the user has the right READ </t:hasRight>
We might need some more components to check if the user has any, has all, has none of the passed rights. But I will leave this as exercise for you.
To summarize, I don’t think that it takes more time to code a simple security solution than to learn Spring security. Especially, as components are so simple to create with Tapestry.
4. How do I do …?
You will find the source code of the following examples in the provided howto project.
Navigation
To understand navigation you have to keep in mind that every page has a corresponding class and a template file. There is one exception, a page class that only generates PDF documents doesn’t need a template.
There are two types of links inside of a template. A page link to navigate directly to another page and an action link to execute a method on the current page before navigating somewhere.
The pageLink navigates directly to the page navigation.
<t:pageLink page="navigation">Navigation example</t:pageLink>
The following actionLink will execute the onActionFromNullResponse
method. Depending on the return value of the method, the same page is rendered again or a redirect to another page happens.
<t:actionLink t:id="nullresponse">Method with null response</t:actionLink>
An action method can return null to stay on the same page. It can return a String representing the name of the page to navigate to or the class of that page.
Object onActionFromStringreponse() { return "NavigationTarget"; } Object onActionFromClassresponse() { return NavigationTarget.class; }
Furthermore, you can ask Tapestry to inject a page instance and return this page instance to navigate to it. This allows to initialize the target page. The following example stores a message in the target page before returning it.
@InjectPage private NavigationTarget target; Object onActionFromInstanceresponse() { target.initialize("Instance response"); return target; }
If you want to pass an ID to a link you can make use of a context.
<t:loop source="1..10" value="current"> <t:actionlink t:id="link" context="current">${current}</t:actionlink> - </t:loop>
This will create a link like http://localhost:8080/howto/navigation.link/4
and pass 4 as parameter to the action method.
public class Navigation { @Property @Persist private String message; @Property private int current; Object onActionFromLink(int passedValue){ message = "Value is "+passedValue; return null; } }
Other return types can be StreamingResponse to render a PDF or an image. In addition there are a number of Ajax responses. The following code snippet is the complete code required to render a PDF.
Object onActionFromPdfFile() { return new StreamResponse() { public String getContentType() { return "application/pdf"; } public InputStream getStream() throws IOException { return getClass().getResourceAsStream("sample.pdf"); } public void prepareResponse(Response response) { response.setHeader("Content-disposition", "Attachment; filename=sample.pdf"); } }; }
Friendly URLs
Tapestry has search engine and human eyes friendly URLs out of the box.
Here is the code.
<t:actionlink t:id="show" context="current">${current}</t:actionlink>
And the result
http://localhost:8080/book.show/4
Templates
Tapestry doesn’t have templates because it doesn’t need them. It is a component based framework and you can use this concept to provide templates. We are going to use Layout as component name, which is a kind of convention. (Hint: The term template is not used in the context of Tapestry here but in context of a template engine.)
The component class
package de.laliluna.example.components; public class Layout {}
Note that the package name is important. Components need to be in a components sub package of the package you have defined as application root in the web.xml
The component template in resource/de/laliluna/example/components/Layout.tml
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"> <head> <meta http-equiv="content-type" content="text/html; charset=utf-8"/> <title>My application</title> </head> <body> <t:body/> </body> </html>
The tag <t:body/>
is the placeholder for a nested component. Note that the template needs to be packaged to WEB-INF/classes/de/laliluna/example/components, the same directory as the component class.
Usage sample
<html t:type="layout" xmlns="http://www.w3.org/1999/xhtml" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"> <p>Hello</p> </html>
The t:type="layout"
attribute transforms the HTML tag into a Layout component element. In your mind replace the <html ..>and </html>
with the content of the Layout.tml
. The <p>Hello</p>
is inserted at the t:body
placeholder.
Simple isn’t it. Let’s make the Layout component more complete. We are going to include our stylesheet and provide a title which needs to be passed as parameter to the Layout component.
Component class
@IncludeStylesheet("context:css/styles.css") public class Layout { @Property @Parameter(required = true, defaultPrefix = BindingConstants.LITERAL) private String title; }
Component template
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"> <head> <meta http-equiv="content-type" content="text/html; charset=utf-8"/> <title>${title}</title> </head>
… rest remains unchanged
Usage sample
<html t:type="layout" t:title="Home page" xmlns="http://www.w3.org/1999/xhtml" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"> <p>hello</p> </html>
The attribute t:title="Home page"
passes the title to the Layout component, which uses it in the template.
Components are very flexible, you might build a Layout out of Layout parts or use inheritance.
Forms and validation
Tapestry allows you to create your forms manually.
<t:form t:id="continentForm" zone="help"> <t:label for="continent"/> : <t:select t:id="continent" model="continents"/> <br/> <input type="submit" value="Continue"/> </t:form>
In addition it provides a powerful component generating forms from a model. I will show you the three files you need to create a complete create dialog for an address.
Domain model:
public class Address { @NonVisual private Integer id; private String name; private String street; private String city; private String country; private Salutation salutation; //… getters and setters
The template:
<html t:type="layout" t:title="Address form" xmlns="http://www.w3.org/1999/xhtml" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"> <p> <t:beaneditform submitlabel="${message:save-label}" object="address"/> </p> </html>
The page class:
public class CreateAddress { @Property private Address address; @Inject private AddressService addressService; Object onSuccess(){ addressService.create(address); return null; } }
And here is the result:
Enums and Date values are supported out of the box. You can change the default behavior of the BeanEditForm component.
Reordering of the form fields
By default the fields are ordered by the order of the getter of the model class. The @ReorderProperties allows to specify the fields which should be shown first in the form.
@ReorderProperties("name,city,street") public class Address { … }
Changing the labels of the form fields
Tapestry will look for a resource named city-label to get the label for the city field. If the label is not available, it will use the capitalized field name as label. So just overwrite the default in the resource file of a page.
Sample from the file CreateAddress.properties
name-label=Surname and lastname street-label=Street and house number
Overwrite the rendering
For every field of the model you may provide your own snippet to be rendered.
<t:beaneditform submitlabel="${message:save-label}" object="address"> <t:parameter name="city"> <label for="city">That's the way we input the city</label> <t:textfield t:id="city" value="address.city"/> </t:parameter> </t:beaneditform>
Further options
It is possible to include or exclude fields from a model. In addition, you might provide a new editor for a special object. Think of an editor for a phone number. Just store phone numbers in a dedicated class – for example Phone – build an editor and automatically you will get the same phone editor in all forms of your application. This is a great approach if you have to build large enterprise applications.
I explained an example in the paragraph Advanced Converting. You can replace existing editors with your own implementation as well, just in case you don’t like the calendar used for Date fields.
Validation
Validation can be done using annotations inside of the model. The following code shows the Address class with validations. The @NonVisual annotation tells Tapestry not to show the ID as input
public class Address { @NonVisual private Integer id; @Validate("required,minlength=3") private String name; @Validate("required") private String city; @Validate("required") private String country; // …
The validation takes place on the server side and if JavaScript is enabled on the client site as well. As explained in the web framework article, this is the way to go. If JavaScript is available don’t send requests to the server but validate on the client. Server side validation should happen only as second step to guaranty validation if JavaScript is disabled or for security reason.
Custom validation
I will show you different options to extend the standard validation.
Annotation based validation
We will extend the standard validators provided by the annotation @Validate . I would like to validate that the name cannot be ‘foo’, because foo is really a bad name.
public class Address { @Validate("required,minlength=3,foo") private String name;
First, create a validator. We can extend the AbstractValidator. The validator will use the message key foo-not-allowed. You might have a look into the source code of the existing validators like Required or MinLength to get some inspiration.
public class FooValidator extends AbstractValidator<Void, String> { public FooValidator() { super(null, String.class, "foo-not-allowed"); } public void validate(Field field, Void constraintValue, MessageFormatter formatter, String value) throws ValidationException { if ("foo". equals(value)) throw new ValidationException(buildMessage(formatter, field, constraintValue)); } private String buildMessage(MessageFormatter formatter, Field field, Void constraintValue) { return formatter.format(constraintValue, field.getLabel()); } public void render(Field field, Void constraintValue, MessageFormatter formatter, MarkupWriter writer, FormSupport formSupport) { formSupport.addValidation(field, "foo", buildMessage(formatter, field, constraintValue), null); } }
Then we have to provide a resource file dedicated to validation messages.
ValidationMessages.properties:
foo-not-allowed=Foo is not allowed
Finally, we need to register the validator and the validation resources in the Tapestry application. By default, Tapestry looks for a class AppModule to configure your application. (http://tapestry.apache.org/tapestry5/guide/conf.html).
Add the following methods to your AppModule class.
public static void contributeValidationMessagesSource(OrderedConfiguration<String> configuration) { configuration.add("fooresource", "de/laliluna/example/components/ValidationMessages"); } public static void contributeFieldValidatorSource(MappedConfiguration<String, Validator> configuration) { configuration.add("foo", new FooValidator()); }
In order to support client side JavaScript validation, a JavaScript snippet needs to be created.
JavaScript myvalidation.js
Tapestry.Validator.foo = function(field, message) { field.addValidator(function(value) { if ('foo' == value) throw message; }); };
And make sure that it is integrated into all pages. Have a look into the class AppModule in the method contributeMarkupRenderer(…) to see how to add a global JavaScript file.
That’s it.
Field validation method
If you don’t use the BeanEditForm component but write your form yourself, you will get an event per field. The field with the HTML id ‘country’ can be validated with the following method inside of a page class.
void onValidateFromCountry(String value) throws ValidationException { if (value.equals("bubble")) throw new ValidationException("Bubble is not a valid country"); }
Cross field validation
After the field validations have been executed, a validateForm event is created. Just add the event listener to your page class.
void onValidateForm() throws ValidationException { if ("bar".equals(address.getName()) && "Germany".equals(address.getCountry())) throw new ValidationException("Bar is not a valid name in Germany"); }
Advanced converting
Enums and Date fields are already supported. You can easily define how the date input should look like. Here is an example if you write the form on your own. It is reading the date in yyyy-MM format.
<t:datefield t:id="moved" t:format="yyyy-MM" value="address.moved"/>
Here is an example reading the format from a resource bundle.
Move-in date in Format ${message:dateFormatSample}
<t:datefield t:id="moved" t:format="${message:dateFormat}" value="address.moved"/>
Resource file CreateCoolAddress.properties
dateFormat=yyyy-MM-dd dateFormatSample=2009-02-23
Finally, an example overwriting the default behavior of the beaneditform component.
<t:beaneditform submitlabel="${message:save-label}" object="address"> <t:parameter name="moved"> <label id="moved-label" for="city">That's the day we moved to the place</label> <t:datefield t:id="moved2" t:format="${message:dateFormat}" value="address.moved"/> </t:parameter> </t:beaneditform>
There is really no problem to adapt the validation to your needs.
Customize converting of input
When building large enterprise application in a consistent way, it should be a best practice to use components or even build your own components in order to achieve a consistent look and feel in all dialogs. Components can be reused all over the place and a fix to a component will fix the problem everywhere in your application.
Tapestry provides you with great support to achieve this goal. In this chapter, we will have a look how to customize a single form and how to build an input element for a new type. I use a Phone class as sample, but you may think of creditcard numbers, article numbers in a specific format or other custom types as well.
If you write out a form instead of using the BeanEditForm component, you can hook into two events to transform the value when it goes to the client and when it comes back from the client
String onToClientFromCountry() { … } Object onParseClientFromCountry(String value) { … }
For a BeanEditForm component you have to follow a different approach. You need to create your own edit block and define a custom translator (= converter).
The translator translates the client input to a Phone object and back again.
PhoneTranslator class
public class PhoneTranslator extends AbstractTranslator<Phone>{ public PhoneTranslator() { super("phone", Phone.class, "phone"); } public String toClient(Phone value) { return String.format("%s/%s/%s", value.getCountryCode(), value.getAreaPrefix(), value.getCode()); } public Phone parseClient(Field field, String clientValue, String message) throws ValidationException { String[] strings = clientValue.split("/"); if(strings.length != 3) throw new ValidationException(message); return new Phone(strings[0], strings[1], strings[2]); } public void render(Field field, String message, MarkupWriter writer, FormSupport formSupport) { } }
The editor rendered inside of a form is basically just a snippet from a page. We create the page CustomPropertyEditBlocks
. It may contain multiple editor blocks
package de.laliluna.example.pages.input; import … public class CustomPropertyEditBlocks { @Property @Environmental private PropertyEditContext context; /* A text field to edit phone numbers */ @Component(parameters = {"value=context.propertyValue", "label=prop:context.label", "translate=prop:phoneTranslator", "validate=prop:phoneValidator", "clientId=prop:context.propertyId", "annotationProvider=context"}) private TextField phonefield; public FieldValidator getPhoneValidator() { FieldValidator fieldValidator = context.getValidator(phonefield); return fieldValidator; } public FieldTranslator getPhoneTranslator() { FieldTranslator translator = context.getTranslator(phonefield); return translator; } }
The corresponding template CustomPropertyEditBlocks.tml
<div xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"> <t:block id="phone"> <t:label for="phonefield"/> <t:textfield t:id="phonefield" size="10"/> </t:block> </div>
Finally, we have to add our edit block to our application configuration. Add the following methods in the class AppModule. We map Phone.class to a type name phoneNumber. Then we add the bean block for phoneNumber. It is described in the block with id phone in the page input/CustomPropertyEditBlocks
. The block makes use of the PhoneTranslator
.
public void contributeDefaultDataTypeAnalyzer(MappedConfiguration<Class, String> configuration) { configuration.add(Phone.class, "phoneNumber"); } public void contributeBeanBlockSource( Configuration<BeanBlockContribution> configuration) { configuration.add(new BeanBlockContribution("phoneNumber", "input/CustomPropertyEditBlocks", "phone", true)); } public void contributeTranslatorSource(Configuration<Translator> configuration) { configuration.add(new PhoneTranslator()); }
This is by far simpler than creating a component with JavaServer Faces 1.x or 2 or writing a Taglibrary for JSPs.
Common dialog tasks
Are more complex input widgets provided like calendars or tree tables.
Yes, there are. The default components provide already grids with paging (alias tables), calendars, pallets (Two boxes allowing to move items from one box to the other) and other options.
In addition there are component collections like http://www.chenillekit.org/ and finally, you can find a lot of component examples in the wiki http://wiki.apache.org/tapestry/Tapestry5HowTos
Is there a simple way to iterate through collections?
There is a loop component, which can loop over collections or loop like a for loop.
<t:loop source="1..10" value="current"> <t:actionlink t:id="link" context="current">${current}</t:actionlink> - </t:loop>
How do I hide a part of the page depending on a condition?
Here is a snippet from a template
<t:loop source="1..10" value="current"> <t:if test="luckyNumber"> ${current} is my lucky number - <p:else> ${current} - </p:else> </t:if> </t:loop>
Tapestry doesn’t support expressions in the test condition but you can provide a method in the corresponding page class.
public boolean isLuckyNumber(){ return current == 7; }
How do I print a sorting table?
You need to provide a datasource. The simplest form of a datasource is a collection.
Page class:
public class VariousComponents {
private List<Address> addresses = new ArrayList<Address>();
public VariousComponents() { addresses.add(new Address("foo", "Bad Vilbel", "Germamy", "Bubenweg 1", Salutation.MR)); addresses.add(new Address("bar", "Aachen", "Germamy", "Neuer Weg 1", Salutation.MR)); addresses.add(new Address("bazz", "Frankfurt", "Germamy", "Neuer Weg 2", Salutation.MR)); addresses.add(new Address("bozz", "Chemnitz", "Germamy", "Hase 1", Salutation.MR)); }
public List<Address> getAddresses() { return addresses; } }
Page template VariousComponents.tml:
<html t:type="layout" t:title="Hello World Page" xmlns="http://www.w3.org/1999/xhtml" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"> <t:grid source="addresses"/> </html>
The result is a sortable table with paging:
The Grid component is quite flexible and easy to use. You can provide a GridDatasource to call your business services to provide the displayed roles. Here is the interface you have to implement, just to give you an idea:
public interface GridDataSource { int getAvailableRows(); /** * Invoked to allow the source to prepare to present values. This gives the * source a chance to pre-fetch data (when appropriate) and * informs the source of the desired sort order. */ void prepare(int startIndex, int endIndex, List<SortConstraint> sortConstraints); /** * Returns the row value at the provided index. */ Object getRowValue(int index); /** * Returns the type of value in the rows, or null if not known. */ Class getRowType();
Tapestry provides ready to use GridDataSource implementation. One can be used to integrate a Hibernate query as datasource provider.
Multi row input and indexed properties
You have two choices. Either you use the Grid component or you write a table on your own. Here is an example using the Grid which allows to edit name and city.
<form t:type="form" t:id="addressForm"> <t:errors/> <t:grid source="addresses" row="address"> <p:nameCell> <t:hidden value="address.id"/> <input t:type="TextField" t:id="name" t:value="address.name" /> </p:nameCell> <p:cityCell> <input t:type="TextField" t:id="city" t:value="address.city"/> </p:cityCell> </t:grid> <input type="submit" value="Save"/> </form>
The corresponding page class
public class VariousComponents {
private List
addresses = new ArrayList();@Property
private Address address;
public VariousComponents() {
addresses.add(
new Address(“foo”, “Bad Vilbel”, “Germamy”, “Bubenweg 1”, Salutation.MR));
addresses.add(
new Address(“bar”, “Aachen”, “Germamy”, “Neuer Weg 1”, Salutation.MR));
addresses.add(
new Address(“bazz”, “Frankfurt”, “Germamy”, “Neuer Weg 2”, Salutation.MR));
// just set an id value
for(int i = 0; i< addresses.size();i++)
addresses.get(i).setId(i+1);
}
public List
getAddresses() {return addresses;
}
}
The example is of course simplified. You can find further information in the editable grid and editable loop examples on http://jumpstart.doublenegative.com.au/
Message resources and internationalization
Every application has per default a central resource bundle file and one per each page. Page resource bundles need to be placed in the same package as the page.
Sample:
de/laliluna/example/pages/MyPage.properties
de/laliluna/example/pages/MyPage_fr.properties
de/laliluna/example/pages/MyPage_de.properties
The suffixes _fr and _de indicates that the resource bundle contains French respectively German messages.
The central resource bundle has to be placed into the WEB-INF folder
Sample:
/WEB-INF/app.properties
/WEB-INF/app_de.properties
The supported languages can be configured. Here is the relevant extract of the AppModule class.
public void contributeApplicationDefaults( MappedConfiguration<String, String> configuration) { configuration.add(SymbolConstants.SUPPORTED_LOCALES, "en,de"); }
Tapestry uses a hierarchical approach to find out a message key. While rendering the MyPage template, for a German locale (de), Tapestry will scan the property files in the following order and return the first found entry.
MyPage_de.properties
MyPage.properties
app_de.properties
app.properties
As a consequence, you can use app.properties and MyPage.properties to provide the messages in the default language.
Sample property file
welcome=Welcome to our foo and bar offer emptystock=No offers available stockinfo=We have %d %s on stock
Inside of a template you can access the messages using the message prefix in an expression.
${message:welcome}
Tapestry can inject the messages into a page class. The following code shows an example to get a plain message and to format a message with parameters.
public class Localization {
@Inject private Messages messages;
public String getFooInfo() { int fooStockLevel = new Random().nextBoolean() ? 3 : 0; if (fooStockLevel == 0) return messages.get("emptystock"); else return messages.format("stockinfo", fooStockLevel, "Foo"); } }
Another nice feature is the localization of templates. The following template is only shown to German speaking users.
Localization_de.tml
<html t:type="layout" t:title="Localization" xmlns="http://www.w3.org/1999/xhtml" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"> <h3>Template for German speaking people - German is great</h3> <p>${message:welcome}</p> <h3>Foo offers</h3> ${fooInfo} </html>
The locale is normally taken from the browser but you can set the local programmatically as well.
Exception handling
Tapestry has a production and a development mode. In development mode you will get a really nice error report showing the line of a template producing the error and explaining in a mostly understandable way the reason for the error.
The following sample error message indicates that our page templates reference a variable foo but it is not provided by the page class. But there is an available property named foos.
Friendly, isn’t it?
In order to replace this error page with a custom page, it is only required to create a page with the same name as the default error page of Tapestry. The page class needs to implement the ErrorReporter interface.
ExceptionReport class
public class ExceptionReport_ implements ExceptionReporter { private Throwable exception; public Throwable getException() { return exception; } public void reportException(Throwable exception) { this.exception = exception; } }
ExceptionReport.tml
<html t:type="layout" t:title="Exception report" xmlns="http://www.w3.org/1999/xhtml" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"> ${exception.message} </html>
Alternatively you can hook into the service handling the exception. Have a look into the documentation for further information.
Post-Redirect-Get and flash scope
Tapestry distinguish between a page link and action links or form actions. A page link will cause a simple GET request of the target page whereas the other links will cause a GET or a POST with an immediate redirect. As a consequence you don’t have to think about the Redirect-on-Post best practice. It is already enabled by default.
If you want to pass an object to be shown after the redirect, you have to annotate the field with the @Persist
annotation.
public class Flash { @Persist(PersistenceConstants.FLASH) private String message; Object onAction() { message = "Hello message for the next request"; return null; }
Conversation context, wizards and workflows
There is no ready to use functionality for wizards or workflows but Tapestry provides the building blocks you need. The name of the functionality is application state. Furthermore, there are plans to integrate the Spring web flow module.
Let’s see how we can use the application state to build a wizard.
We are going to build an input wizard for an address. There are two input steps. In order to distinguish multiple wizards opened in multiple browser tabs, every wizard receives an id. This id will be passed around with every form submit or action link.
Usage
// ------ Start page ----- @InjectPage private Step1 step1; public Object onAction() { startDialog(new Article()); return continueDialog(step1); } // ------ Step1 page ------ public class Step1 extends AbstractWizard<Article> { @InjectPage private Step2 step2; public Object onSuccess() { return continueDialog(step2); } // ------ Step2 page ------ public class Step2 extends AbstractWizard<Article> { @InjectPage private End end; @Inject private ArticleService articleService; public Object onSuccess() { Article article = getModel(); articleService.create(article); endDialog(); return end.initialize(article.getId()); } }
All pages need to extend the AbstractWizard
page
The abstract wizard page provides method to deal with the dialog. The annotation @SessionState
let Tapestry store an instance of DialogState in the session. The DialogState class has a Map of dialog ids and corresponding models. Basically, it is just the storage of the currently edited articles.
public abstract class AbstractWizard<T> { @SessionState private DialogState<T> dialogState; public T getModel() { return dialogState.getBean(dialogId); } public void startDialog(T bean) { dialogId = dialogState.startDialog(bean); } public Object continueDialog(AbstractWizard page) { page.dialogId = dialogId; return page; } public void endDialog() { dialogState.endDialog(dialogId); // set to null if empty to let Tapestry destroy the dialogState if (dialogState.empty()) dialogState = null; } // …
Furthermore, there are methods to guaranty that Tapestry will include the dialogId in forms and action links.
private Integer dialogId; public Integer getDialogId() { if(dialogId == null) throw new IllegalStateException("Dialog id is null"); return dialogId; } Integer onPassivate(){ return dialogId; } void onActivate(Integer dialogId) { this.dialogId = dialogId; } }
As you can see it is not very difficult to provide the wizard/dialog functionality.
Double submit handling
There is no build in mechanism to prevent a double submit. I would like to show an example using the provided mechanism to extend Tapestry.
We are going to build a mixin for the form components A mixin provides functionality which can easily be added to an existing component.
Page template sample using the mixin
<html t:type="layout" t:title="Address form" xmlns="http://www.w3.org/1999/xhtml" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"> <t:form t:mixins="doublesubmit"> <t:textField t:id="foo" value="foo"/> <input type="submit" value="Submit"/> </t:form> </html>
Mixins have to be created in the mixins package. The mixin will call a service generating a unique id for the form submit and add a submit id as hidden field to the form.
Mixin class
package de.laliluna.example.mixins; @MixinAfter public class DoubleSubmit { @Inject private FormSubmitIdService serviceSubmit; void beginRender(MarkupWriter writer){ writer.element("input", "type", "hidden", "name", SUBMIT_ID, "value", serviceSubmit.generateId()); writer.end(); } public static final String SUBMIT_ID = "_submitid"; }
Finally, we will hook into the request processing to check if the id is valid or not. The first submit will check successfully and immediately remove the id from the list of available ids. A double submit of the same form, will have a second submit request, which check will fail as the id is already removed.
DoubleSubmitFilter class
public class DoubleSubmitFilter implements RequestFilter { private FormSubmitIdService formSubmitIdService; public DoubleSubmitFilter(FormSubmitIdService formSubmitIdService) { this.formSubmitIdService = formSubmitIdService; } public boolean service(Request request, Response response, RequestHandler handler) throws IOException { String parameter = request.getParameter(DoubleSubmit.SUBMIT_ID); if (parameter != null) { try { int id = Integer.parseInt(parameter); if (!formSubmitIdService.checkAndRemove(id)) throw new IllegalStateException(String.format("Form id %d is not valid.", id)); } catch (NumberFormatException e) { throw new IllegalStateException(String.format("Form id %s is not valid.", parameter)); } } return handler.service(request, response); } }
And here is the integration of the filter into our Tapestry application.
Extract of the AppModule class
public DoubleSubmitFilter buildDoubleSubmitFilter(final FormSubmitIdService formSubmitIdService){ return new DoubleSubmitFilter(formSubmitIdService); } public void contributeRequestHandler(OrderedConfiguration<RequestFilter> configuration,@Local DoubleSubmitFilter doubleSubmitFilter) { configuration.add("Double submit", doubleSubmitFilter); }
The FormSubmitIdService
generates unique ids and has a thread safe list containing the ids. Have a look into the provided source code.
Custom components
I have stated that it is very simple to write a custom component in Tapestry. Here are three examples to base this opinion.
The first component should display ‘Hello World’, the second component displays content randomly and the third is somehow more usable and allows to toggle (show/hide) a part of the page by clicking.
Hello World component
A component requires a component class, which need to be in the components package.
If you have specified the package de.laliluna.example
as tapestry app package in your web.xml, then your components package is de.laliluna.example.components
.
Every component requires a component class. We are going to create an empty class in that package.
package de.laliluna.example.components; public class HelloWorld { }
In the same package but in the resource directory, create the template file HelloWorld.tml
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"> <b>Hello world, dude!</b> </html>
Now we can use the component in a page.
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"> <head> <meta http-equiv="content-type" content="text/html; charset=utf-8"/> <title>Sample components</title> </head> <body> <t:helloWorld/> </body> </html>
Directory Structure in Tapestry
Tapestry expects that component classes and templates are both packaged to the same folder. A template does not belong to the web pages directory. The proposed structure is to have a resource directory holding templates and resource bundle files and a src directory holding the java classes.
/resources/de/laliluna/example/components /src/de/laliluna/example/components
Both are packaged to the same directory
/WEB-INF/classes/de/laliluna/example/components
Random display component
The component shows a part of the page randomly. The component class returns true or false by random. If it returns falls, then the body is not rendered.
Component class
public class Random { private java.util.Random random = new java.util.Random(); @BeforeRenderBody boolean renderMessage() { return (random.nextBoolean()); } }
The component template has a t:body tag to render the body of the component.
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"> <t:body/> </html>
Usage example
<t:random>We have been lucky.</t:random>
Toggle component
The component allows to specify a target element which can be shown and hidden by a click.
The component has two parameters, one is the id of the target element and the other one the component id of the click element.
As the component might be used repeatedly, we let Tapestry calculate a unique id in the setupRender method. In the beginRender we start to render the div, then the body is rendered and finally in the afterRender method the div is closed and a JavaScript snippet is added to the page. The JavasScript code uses Prototype to hide the target element initially and to register a click event to toggle the target element.
Component class
package de.laliluna.example.components; public class Toggle { @Parameter(required = true, defaultPrefix = BindingConstants.LITERAL) private String targetId; @Parameter(value = "prop:componentResources.id", defaultPrefix = BindingConstants.LITERAL) private String clientId; private String calculatedClientId; @Environmental private RenderSupport renderSupport; void setupRender() { calculatedClientId = renderSupport.allocateClientId(clientId); } void beginRender(MarkupWriter writer) { writer.element("div", "id", calculatedClientId); } void afterRender(MarkupWriter writer) { writer.end(); renderSupport.addScript("$('%s').hide();$('%s').observe('mouseover', function(event){ event.element().setStyle({cursor:'pointer'});" + "});Event.observe('%s', 'click', function(event) {$('%s').toggle();});", targetId, calculatedClientId, calculatedClientId, targetId); } }
Template Toggle.tml
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:t="http://tapestry.apache.org/schema/tapestry_5_1_0.xsd"> <t:body/> </html>
Usage example
<t:toggle targetId="target">Click here to toggle following text</t:toggle> <div id="target">A text</div>
Performance and scalability
This chapter is only included in the PDF document framework-evaluation-performance.pdf.