Extending Template Lookup Mechanism
As you probably know Tapestry’s localization support provides localized component templates. A page or a component may have several locale-specific templates. When rendering a component Tapestry will search for a localized version of each component template and use the closest match. For example you could have MyPage_fr.tml for French users, MyPage_de.tml for German users and MyPage.tml for all other users.
In my Tapestry workshops I’m often asked how to extend the template lookup mechanism. Some Tapestry based applications have a use case to provide different flavors of a single page template. Imagine you want your app do be used on mobile phones. In this case you need to provide an alternative rendering for your pages. It is almost impossible to  create a template which looks good on standard web browser and mobile devices. Thus you need to provide different templates for a single page.
Extending Template Lookup Mechanism
This is the point at which Tapestry’s extensibility comes into play. As you know you can easily override almost every part of Tapestry by replacing a particular service. Now let me show you how to extend Tapestry’s template lookup mechanism in Tapestry 5.2. The discussed mechanism was also available in prior versions but it was a part of the internal API. You can use the  internal API only at your own risk because we can change in the future releases. In 5.2 this mechanism became a public API. Now it is safe to extend it without to fear any upgrade problems. Let’s see it.
The key part of Tapestry’s template lookup mechanism is the ComponentTemplateLocator service which is used to locate both page and component templates. The service implementation is a chain-of-command and can be configured by ComponentTemplateLocator instances. Tapestry contributes two implementations of ComponentTemplateLocator to the chain:
- Default: Searches for a template on the classpath
- Page: Searches for page templates in the application context (root folder of your WAR file)
 Now let’s contribute a command which will locate a page template based on a classifier. Such a classifier can be a string identifying the flavor of the template to render the page. For example if you want to render the page MyPage on iPhone you may provide a classifier like iphone to retrieve the template MyPage_iphone.tml. If the client is on the Palm phone, the classifier palm can be used to retrieve the template MyPage_palm.tml. If the classifier is not available, the default template MyPage.tml will be rendered.
Let’s create a per-thread service named TemplateClassifierSource, which is responsible for identifying the client and providing a corresponding classifier. The implementation details are not interesting.
public interface TemplateClassifierSource {
   String getClassifier();
}
Our implementation of ComponentTemplateLocator will use the TemplateClassifierSource service to identify the proper template for the page. In the following example we first retrieve the page class name and resolve the page name from it. Then we calculate the name of the template resource and load the resource from context root. We also try to find a localized version of the template. Thus we can provide templates like MyPage_iphone_de.tml. If no template was found we return null. In this case the next command in the ComponentTemplateLocator chain will try to locate the template.
public class ClassifierPageTemplateLocator
implements ComponentTemplateLocator {
@Inject
@ContextProvider
private AssetFactory contextAssetFactory;
@Inject
private ComponentClassResolver resolver;
@Inject
private TemplateClassifierSource classifierSource;
   public Resource locateTemplate(ComponentModel model,
Locale locale) {
String className = model.getComponentClassName();
if (!className.contains(".pages.")) {
return null;
}
String logicalName = this.resolver
.resolvePageClassNameToPageName(className);
String path = String.format("%s_%s.%s",
logicalName,
this.classifierSource.getClassifier(),
TapestryConstants.TEMPLATE_EXTENSION);
Resource contextRoot =
this.contextAssetFactory.getRootResource();
Resource resource = contextRoot.forFile(path);
if (resource != null) {
return resource.forLocale(locale);
}
return null;
}
}
Contributing ComponentTemplateLocator
Next we contribute our ComponentTemplateLocator before the locator with id Page.
public class AppModule {
public static void bind(ServiceBinder binder) {
binder.bind(TemplateClassifierSource.class,
TemplateClassifierSourceImpl.class);
}
public static void contributeComponentTemplateLocator(
OrderedConfiguration<ComponentTemplateLocator> config) {
config.addInstance("Classifier",
ClassifierPageTemplateLocator.class,
"before:Page");
}
}
Comparing To JSF Renderers
If you are familiar with JavaServer Faces technology, you will recognize that the described mechanism is comparable to JSF Component Renderers. Both Tapestry and JSF allow you to render a component in different ways. The big difference is the fact that JSF Renderers don’t have templates. You code the rendering of the component inside your Java classes. Isn’t it the reason why we don’t use Servlets anymore? However, in Tapestry you define the look and feel of a component in a template. Another disadvantage of JSF Renderers is that they are limited to components. There is no way in JSF to achieve the same for a Facelet page.
Summary
In this article you learned how to extend Tapestry’s template lookup mechanism. Note that we covered only the lookup of page templates from application context root. You can enhance the mechanism to locate component templates on the classpath.
The described mechanism is very flexible. Instead of coding a classifier into the template name, one could store the specific templates into a subfolder and use the classifier as folder name.
Thanks for the useful article but I think there is a problem with this approach. You define a per-thread TemplateClassifierSource, but the page template is cached by Tapestry based on a key composed of just the component name and locale. See ComponentTemplateSourceImpl.getTemplate(). So for different requests the same template will be returned after the first one is resolved.
Have not actually tried this in 5.2 but did something similar in 5.1 and hit the caching issue.
I think to make this work properly the locale which is used quite a bit in the template resolution area needs to be generalised to a ‘context’ which includes the locale and anything else an app wants to put in there per request.
Hi Alfie,
you are absolutely right. Right now there is a bit more work needed to extend the template lookup mechanism. I was forced to override a bunch of classes from the internal API responsible for caching various objects. In 5.3 I’ll provide some public API for it.
See this thread: http://markmail.org/thread/d45pg6jpjc62e4qg