Less Configuration - Enhanced Tapestry Component Resolution.

I like to be able to organize files into groups with folders and I also don't like to do too much configuration file maintenance. Tapestry will find components in the /WEB-INF directory in my WAR without configuration but with many pages and components in an application it becomes unwieldy. Tapestry allows me to add these components to my specification, specifying the path, relative to the /WEB-INF folder, and t-deli.com provides an ant-task to automate the maintenance of these component declarations. What if I could structure my pages and components into any directory structure I want and not have worry about maintaining the application specification file, even with an automated tool? I could add components while testing without a restart. Since servlet containers can explode a deployed WAR file into a directory structure this method would even work in a deployed Tapestry application.

Fortunately, Tapestry has a number of extension points which can be used to extend various framework behaviors. One of these is the ISpecificationResolverDelegate interface. I can implement this interface and add an extension into my application configuration:

<extension name="org.apache.tapestry.specification-resolver-delegate" class="com.mjhenderson.users.tapestry.CustomSpecificationResolver" immediate="yes">

The interface consists of only 2 methods:

public IComponentSpecification findPageSpecification( IRequestCycle cycle, INamespace namespace, String simplePageName); public IComponentSpecification findComponentSpecification( IRequestCycle cycle, INamespace namespace, String type);

The delegate is Tapestry's last resort for resolving a component specification which is not found by the built-in resolution methods. It allows Tapestry component specifications to be derived by any means, constructed dynamically, retrieved from a RDBMS, whatever you can think of. The delegate is also responsible for caching component specifications. Tapestry will not cache component specifications resolved by the delegate.

All I want to do is to be able to add components to my application, in any directory structure I find convenient under /WEB-INF and not have to maintain the application specification. This has one immediate benefit, I keep forgetting to edit the file and run into problems when I run the app, I have to stop, edit the specification and restart the app. The delegate implementation prevents this error.

However the major advantage, from my point of view is less configuration, I get to maintain components in a logical directory structure and let the framework do the leg-work of finding them at runtime.

My resolver delegate implementation is fairly simple, each of the methods is only a few lines long:

public IComponentSpecification findPageSpecification(IRequestCycle cycle, INamespace namespace, String name) { if (!_initialized) { _init(cycle); } if (!_applicationIsExplodedWAR) { return null; } IResourceLocation location = _locator.getSpecificationLocation(name+".page"); if (location != null) { return _specificationSource.getPageSpecification(location); } return null; }

The delegate must be initialized and it cannot be done in the no-argument constructor, it requires access to the IRequestCycle:

private void _init(IRequestCycle cycle) { IResourceLocation rootLocation = Tapestry.getApplicationRootLocation(cycle).getRelativeLocation("/WEB-INF/"); _applicationIsExplodedWAR = rootLocation.getResourceURL().toString().startsWith("file:"); if (_applicationIsExplodedWAR) { _locator = new RecursiveFileLocator(rootLocation); _specificationSource = cycle.getEngine().getSpecificationSource(); } _initialized = true; }


The RecursiveFileLocator is responsible for resolving components in a single directory. It maintains a Map of child locators for each of it's sub-directories. It locates a specification by first looking in the folder it represents and, if the file is not found, forwarding the request on to each of it's child locators until the file with the expected name is found:

public IResourceLocation getSpecificationLocation(String expectedName) { IResourceLocation location = _resolveThisFolderSpecificationLocation(expectedName); if (location == null) { location = _resolveChildFolderSpecificationLocation(expectedName); } return location; } private IResourceLocation _resolveThisFolderSpecificationLocation(String expectedName) { IResourceLocation location = (IResourceLocation)_locations.get(expectedName); if (location == null) { location = _location.getRelativeLocation(_location.getPath()+"/"+expectedName); if (location.getResourceURL() == null) { return null; } _locations.put(expectedName, location); } return location; } private IResourceLocation _resolveChildFolderSpecificationLocation(String expectedName) { List children = _getChildFolderLocators(); Iterator iterator = children.iterator(); while (iterator.hasNext()) { RecursiveFileLocator child = (RecursiveFileLocator)iterator.next(); IResourceLocation location = child.getSpecificationLocation(expectedName); if (location != null) { return location; } } return null; }


This solution works well for me in development with Eclipse and JettyLauncher and on Tomcat where a deployed WAR file is exploded. If the servlet container you use does not explode the WAR file this method will not work.

There are a couple of shortcomings, the locator caches locations of components and the delegate caches specifications so I can't move a component from one folder to another without restarting my application and I can't create a new sub-folder in my file hierarchy without a restart. These could be worked on until specification location can be manipulated in the IDE while running and testing.

I looked over the Tapestry component resolution code to see how and when the delegate is invoked and I should be able to patch the built-in component resolution to use the RecursiveFileLocator and dispense with the delegate altogether.