Integrating Spring, Velocity and Tiles
Goals
When I use tiles, I don’t like the default (?) approach of tiles.xml
. I don’t want to put imports of JS & CSS, page title, navigation, body etc. each in its own file like in the snippet below, because that makes me switch between editor windows.
<definition name='hello' template='/WEB-INF/templates/main.jsp'> <put-attribute name='title' value='Hello' type='string' /> <put-attribute name='head' value='/WEB-INF/templates/hello-js-and-css.jsp' /> <put-attribute name='nav' value='/WEB-INF/templates/hello-nav.jsp' /> <put-attribute name='body' value='/WEB-INF/templates/hello.jsp' /> </definition>
Obviously I don’t want to put too much detail in tiles.xml
, either.
What I really like is having one file per page, assembling the template in one place, such as this piece of JSP:
<tiles:insertTemplate template='template.jsp'> <tiles:putAttribute name='title' value='Hello' /> <tiles:putAttribute name='head'> <script type='text/javascript' src='/js/jQuery.js' /> <script type='text/javascript' src='/js/hello.js' /> </tiles:putAttribute> <tiles:putAttribute name='body'> <div>Hello, world!</div> </tiles:putAttribute> </tiles:insertTemplate>
In Velocity, it’s supposed to look like this:
#tiles_insertTemplate({'template': 'template.vm'}) #tiles_putAttribute({'name':'title', 'value': 'Hello'})#end #tiles_putAttribute({'name':'head'}) <script type='text/javascript' src='/js/jQuery.js' /> <script type='text/javascript' src='/js/hello.js' /> #end #tiles_putAttribute({'name':'body'}) <div>Hello, world!</div> #end #end
However, the docs on integration are really intended for adding some Velocity support to your Tiles-based app, while I wanted quite the opposite: Use Tiles in my rich Velocity app, with full support for spring context, macros etc.
Solution
In short, what we’re going to do is:
- Use
VelocityViewResolver
to resolve and render pages - Add support for Tiles macros to this Velocity rendering engine
- Extend the Tiles renderer with full support for Velocity, including Spring context, macros etc. Ultimately we’re going to make it use the original Velocity engine created by Spring.
Full source code in form of a minimal, complete web app are at github. For details see below.
Spring & Velocity -> Tiles
For the first step, we define viewResolver
and velocityConfig
like this:
@Bean public VelocityConfig velocityConfig() { VelocityConfigurer cfg = new VelocityConfigurer(); cfg.setResourceLoaderPath('/WEB-INF/velocity/'); cfg.setConfigLocation(context .getResource('/WEB-INF/velocity.properties')); return cfg; } @Bean public ViewResolver viewResolver() { VelocityViewResolver resolver = new VelocityViewResolver(); resolver.setViewClass(VelocityToolboxView.class); resolver.setSuffix('.vm'); return resolver; }
It’s important that we use VelocityToolboxView
there, otherwise the tiles directives won’t work.
We also need to put the following in velocity.properties
:
userdirective=org.apache.tiles.velocity.template.AddAttributeDirective,\ org.apache.tiles.velocity.template.AddListAttributeDirective,\ org.apache.tiles.velocity.template.DefinitionDirective,\ org.apache.tiles.velocity.template.GetAsStringDirective,\ org.apache.tiles.velocity.template.ImportAttributeDirective,\ org.apache.tiles.velocity.template.InsertAttributeDirective,\ org.apache.tiles.velocity.template.InsertDefinitionDirective,\ org.apache.tiles.velocity.template.InsertTemplateDirective,\ org.apache.tiles.velocity.template.PutAttributeDirective,\ org.apache.tiles.velocity.template.PutListAttributeDirective
This adds basic support for Tiles directives to Velocity, but it’s still useless because once Velocity hands rendering over to Tiles, Tiles is unable to render Velocity and would simply ignore it (rendering syntax of #directives
to browser.
Tiles -> Velocity
We need to teach Tiles to use Velocity. For this we’re going to need a custom TilesInitializer
:
@Bean public TilesConfigurer tilesConfigurer() { TilesConfigurer cfg = new TilesConfigurer(); cfg.setTilesInitializer(new VelocityTilesInitializer(velocityConfig())); return cfg; }
public class VelocityTilesInitializer extends DefaultTilesInitializer { private VelocityConfig velocityConfig; public VelocityTilesInitializer(VelocityConfig velocityConfig) { this.velocityConfig = velocityConfig; } @Override protected AbstractTilesContainerFactory createContainerFactory( TilesApplicationContext context) { return new BasicTilesContainerFactory() { @Override protected List<TilesRequestContextFactory> getTilesRequestContextFactoriesToBeChained( ChainedTilesRequestContextFactory parent) { List<TilesRequestContextFactory> factories = super .getTilesRequestContextFactoriesToBeChained(parent); registerRequestContextFactory( VelocityTilesRequestContextFactory.class.getName(), factories, parent); return factories; } @Override protected AttributeRenderer createTemplateAttributeRenderer( BasicRendererFactory rendererFactory, TilesApplicationContext applicationContext, TilesRequestContextFactory contextFactory, TilesContainer container, AttributeEvaluatorFactory attributeEvaluatorFactory) { ContextPassingVelocityAttributeRenderer var = new ContextPassingVelocityAttributeRenderer( velocityConfig.getVelocityEngine()); var.setApplicationContext(applicationContext); var.setRequestContextFactory(contextFactory); var.setAttributeEvaluatorFactory(attributeEvaluatorFactory); var.commit(); return var; } }; } }
We’re almost there, but here’s a tricky bit. Normally in lines 31-32 you would put velocityAttributeRenderer
. However, this renderer completely ignores the Spring-augmented Velocity context & engine that Tiles received from Velocity. It creates its own VelocityEngine
and lets it do the rendering, throwing away all the Spring and tiles directives and context objects.
There is no way to change this behavior in Tiles (which otherwise seems to be an interesting study in design patterns and extensibility). I even created two JIRA issues for it: 541 for forwarding context and 542 for injecting VelocityEngine
.
Meanwhile, we have to make do with this workaround (see github for full source):
public class ContextPassingVelocityAttributeRenderer extends AbstractTypeDetectingAttributeRenderer { // ... private VelocityEngine engine; public ContextPassingVelocityAttributeRenderer(VelocityEngine engine) { this.engine = engine; } // ... public void commit() { velocityView = new VelocityView(new TilesApplicationContextJeeConfig()); velocityView.setVelocityEngine(engine); } @Override public void write(Object value, Attribute attribute, TilesRequestContext request) throws IOException { if (value != null) { if (value instanceof String) { InternalContextAdapter adapter = (InternalContextAdapter) ((VelocityTilesRequestContext) request) .getRequestObjects()[0]; Context context = adapter.getInternalUserContext(); Template template = velocityView.getTemplate((String) value); velocityView.merge(template, context, request.getWriter()); } else { throw new InvalidTemplateException( 'Cannot render a template that is not a string: ' + value.toString()); } } else { throw new InvalidTemplateException('Cannot render a null template'); } } // ...
It works arounds both of the JIRA issues and lets us accomplish the ultimate goal:
- The
VelocityEngine
injected toVelocityView
here is the originalVelocityEngine
from Spring. Among other things, it supports Spring directives and context-dependent tools. - The
TilesRequestContext
inwrite
method still contains the original Velocity context created from Spring scaffolding. Standard implementation ofVelocityAttributeRenderer
simply throws it away. This workaround above extracts the original context and uses it for rendering.
Conclusion
This journey took much more time than I thought. Documentation on such cases is nonexistent, so I spent hours debugging, reading source code, experimenting, praying and cursing. It was even more fun as I had close to zero knowledge of internals of Spring view resolution and rendering engine, as well as Tiles and Velocity.
It’s quite satisfying since I learned a ton about all those technologies and eventually was able to resolve it in a fairly elegant way. But it was also a frustrating and time-consuming riddle and I hope this post spares someone some trouble.
Update – Velocity Tools
A while later I discovered this solution does not support Velocity Tools property. Here’s how to add it: Spring & Velocity Tools.
Reference: Integrating Spring, Velocity and Tiles from our JCG partner Konrad Garus at the Squirrel’s blog.