A couple of weeks ago I’ve stumbled upon a problem of integrating Tiles and Velocity under Spring MVC. It appears that there is no out-of-the-box solution to this. Tiles can be used in Velocity through the Velocity Tools, but not under Spring. The reason why I needed this integration is because of advanced templating features of Tiles - template inheritance, for instance, and featured of Velocity - ease of use and high performance as compared to JSP/JSTL.
So here is the solution.
I assume that all templates and resources are placed in the WEB-INF directory.
Create a Velocity template, which will be the placeholder for your tiles (WEB-INF/layouts/template.vm):
<html>
<head>
</head>
<body>
#parse($tiles.getAttribute("header"))
#parse($tiles.getAttribute("central"))
#parse($tiles.getAttribute("bottom"))
</body>
</html>The header, central and bottom are the names of the tiles attributes we will be inserting.
The tiles definition file (tiles-defs.xml) looks like this:
<!DOCTYPE tiles-definitions PUBLIC
"-//Apache Software Foundation//DTD Tiles Configuration 1.3//EN"
"http://struts.apache.org/dtds/tiles-config_1_3.dtd">
<tiles-definitions>
<definition name="main_base" page="/layouts/template.vm">
<put name="header" value="/layouts/header.vm" />
</definition>
<definition name="page.tile" extends="main_base">
<put name="central" value="messages.vm" />
</definition></tiles-definitions>
Then goes the Spring configuration:
For tiles bean
<bean id="tilesConfigurer" class="org.springframework.web.servlet.view.tiles.TilesConfigurer">
<property name="factoryClass" value="org.apache.struts.tiles.xmlDefinition.I18nFactorySet"/>
<property name="definitions">
<list>
<value>/WEB-INF/config/tiles-defs.xml</value>
</list>
</property>
</bean>For the velocity view resolver and engine
<bean id="velocityConfig" class="org.springframework.web.servlet.view.velocity.VelocityConfigurer">
<property name="resourceLoaderPath" value="/WEB-INF/views" />
<property name="velocityProperties">
<props>
<prop key="input.encoding">utf-8</prop>
<prop key="output.encoding">utf-8</prop>
</props>
</property>
</bean>
<bean id="viewResolver" class="org.springframework.web.servlet.view.velocity.VelocityViewResolver">
<property name="cache" value="true" />
<property name="prefix" value="" />
<property name="exposeSpringMacroHelpers" value="true" />
<property name="order" value="1"></property>
<property name="contentType" value="text/html; charset=utf-8"></property>
<property name="toolboxConfigLocation" value="WEB-INF/config/velocity-toolbox.xml"></property>
<property name="suffix" value=".tile" />
<property name="viewClass" value="com.yoursite.VelocityTilesView" />
</bean>Don’t forget to create Velocity tools config:
<?xml version="1.0"?>
<toolbox>
<tool>
<key>tiles</key>
<scope>request</scope>
<class>org.apache.velocity.tools.struts.TilesTool</class>
</tool>
</toolbox>And finally create a custom view class, which will actually do the job of interpreting the Velocity/Tiles templates:
package com.yoursite;
import java.util.Map;
import javax.servlet.*;
import javax.servlet.http.*;
import org.apache.struts.tiles.*;
import org.apache.velocity.context.Context;
import org.springframework.context.ApplicationContextException;
import org.springframework.web.servlet.view.velocity.VelocityToolboxView;
import org.springframework.web.util.WebUtils;
public class VelocityTilesView extends VelocityToolboxView {
/**
* Name of the attribute that will override the path of the layout page to render. A Tiles
* component controller can set such an attribute to dynamically switch the look and feel of a
* Tiles page.
*
* @see #setPath
*/
public static final String PATH_ATTRIBUTE = VelocityTilesView.class.getName() + ".PATH";
private DefinitionsFactory definitionsFactory;
/**
* Constructor for use as a bean.
*/
public VelocityTilesView() {}
/**
* Create a new InternalResourceView with the given URL.
*/
public VelocityTilesView(String url) {
setUrl(url);
}
/**
* Render the internal resource given the specified model. This includes setting the model as
* request attributes.
*/
protected void renderMergedTemplateModel(Map model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
exposeHelpers(model, request);
Context velocityContext = createVelocityContext(model, request, response);
exposeHelpers(velocityContext, request, response);
exposeToolAttributes(velocityContext, request);
exposeModelAsRequestAttributes(model, request);
String tile = prepareForRendering(request, response);
mergeTemplate(getTemplate(tile), velocityContext, response);
}
/**
* Determine whether to use RequestDispatcher's <code>include</code> or <code>forward</code>
* method.
* <p>
* Performs a check whether an include URI attribute is found in the request, indicating an
* include request, and whether the response has already been committed. In both cases, an
* include will be performed, as a forward is not possible anymore.
*/
protected boolean useInclude(HttpServletRequest request, HttpServletResponse response) {
return (WebUtils.isIncludeRequest(request) || response.isCommitted());
}
/**
* Expose the current request URI and paths as {@link HttpServletRequest} attributes under the
* keys defined in the Servlet 2.4 specification, for Servlet 2.3-containers.
* <p>
* Does not override values if already present, to not conflict with Servlet 2.4+ containers.
*/
protected void exposeForwardRequestAttributes(HttpServletRequest request) {
WebUtils.exposeForwardRequestAttributes(request);
}
/**
* Overrides checkTemplate in VelocityView to avoid throwing an exception if it can't find a
* file.
*/
protected void checkTemplate() throws ApplicationContextException {
// skip check.
}
/**
* Set the path of the layout page to render.
*/
public static void setPath(HttpServletRequest request, String path) {
request.setAttribute(PATH_ATTRIBUTE, path);
}
protected void initApplicationContext() throws ApplicationContextException {
super.initApplicationContext();
// get definitions factory
this.definitionsFactory = (DefinitionsFactory) getServletContext().getAttribute(
TilesUtilImpl.DEFINITIONS_FACTORY);
if (this.definitionsFactory == null) {
throw new ApplicationContextException(
"Tiles definitions factory not found: TilesConfigurer not defined?");
}
}
/**
* Prepare for rendering the Tiles definition: Execute the associated component controller if
* any, and determine the request dispatcher path.
*/
protected String prepareForRendering(HttpServletRequest request, HttpServletResponse response)
throws Exception {
// get component definition
ComponentDefinition definition = getComponentDefinition(this.definitionsFactory, request);
if (definition == null) {
throw new ServletException("No Tiles definition found for name '" + getUrl() + "'");
}
// get current component context
ComponentContext context = getComponentContext(definition, request);
// execute component controller associated with definition, if any
Controller controller = getController(definition, request);
if (controller != null) {
if (logger.isDebugEnabled()) {
logger.debug("Executing Tiles controller [" + controller + "]");
}
executeController(controller, context, request, response);
}
// determine the path of the definition
String path = getDispatcherPath(definition, request);
if (path == null) {
throw new ServletException("Could not determine a path for Tiles definition '"
+ definition.getName() + "'");
}
return path;
}
/**
* Determine the Tiles component definition for the given Tiles definitions factory.
*/
protected ComponentDefinition getComponentDefinition(DefinitionsFactory factory,
HttpServletRequest request) throws Exception {
return factory.getDefinition(getUrl(), request, getServletContext());
}
/**
* Determine the Tiles component context for the given Tiles definition.
*/
protected ComponentContext getComponentContext(ComponentDefinition definition,
HttpServletRequest request) throws Exception {
ComponentContext context = ComponentContext.getContext(request);
if (context == null) {
context = new ComponentContext(definition.getAttributes());
ComponentContext.setContext(context, request);
} else {
context.addMissing(definition.getAttributes());
}
return context;
}
/**
* Determine and initialize the Tiles component controller for the given Tiles definition, if
* any.
*/
protected Controller getController(ComponentDefinition definition, HttpServletRequest request)
throws Exception {
return definition.getOrCreateController();
}
/**
* Execute the given Tiles controller.
*/
protected void executeController(Controller controller, ComponentContext context,
HttpServletRequest request, HttpServletResponse response) throws Exception {
controller.execute(context, request, response, getServletContext());
}
/**
* Determine the dispatcher path for the given Tiles definition, i.e. the request dispatcher
* path of the layout page.
*/
protected String getDispatcherPath(ComponentDefinition definition, HttpServletRequest request)
throws Exception {
Object pathAttr = request.getAttribute(PATH_ATTRIBUTE);
return (pathAttr != null ? pathAttr.toString() : definition.getPath());
}
}Basically this is it. You can now create a ModelAndView instance with the view set to page to display the template defined earlier in the tiles-defs.xml.
PS: recently a new version of Tiles has been released, it is finally decoupled from Struts. I wrote a plugin which integrates Spring MVC, Velocity and now Tiles 2 - welcome here.
First published on the old anydoby.com blog on 2007-06-11.