↓ Skip to main content

Integrating: Spring + Velocity + Tiles

·1036 words·5 mins
Sergii Zolotarov
Author
Sergii Zolotarov
Twenty years of Java, from Kyiv to Leeuwarden. Currently at Booking.com

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.