Struts2 StringTemplate support

UPDATE: There's a more recent version of this post at my blog http://prettyprint.me/2009/11/13/stringtemplate-a-step-forward-to-a-better-web-mvc/

StringTempalte (http://www.stringtemplate.org/) is a nice templating engine with implementations in Java, C# and python.
Struts2 (http://struts.apache.org/2.x/) is a web MVC framework evolved from both struts1 and WebWork (http://www.opensymphony.com/webwork/)

Struts2 has several template engines you can pick from, such as the default jsp, Velocity or Freemarker. However, none of the mentioned engines is strict, well designed and fun to use as StringTemplate. StringTemplate is well thought out and very intuitive to use. It makes code reuse in templates a breeze and enforces good separation b/w the model, the view and the controller.

So I decided I'd like to use StringTemplate with struts2. To do that I've implemented a struts2 result-type and configured my struts.xml.
This code is still experimental, so it's not anywhere public yet, but once I'm happy with it in production I hope to be able to commit it to struts2 and make it world-available.
In the mean time, feel free to just copy the code.

struts.xml:

<struts>

  <include file="webwork-default.xml" />

 

  <package name="regular" namespace="/" extends="struts-default">

    <result-types>

        <result-type name="stringtemplate" class="org.apache.struts2.dispatcher.StringTemplateResult"/>

    </result-types>

    <action name="st"

      class="com.mysite.StringTemplateTestAction">

      <result name="success" type="stringtemplate">/WEB-INF/st/page.st</result>

    </action>

  </package>

</struts>

And the source code:

package org.apache.struts2.dispatcher;

 

import java.util.Map;

 

/**

 * Interface defining the required behavior from a StringTemplate result type.

 * <p>

 * The result should prepare the model for the page to display.

 * The model is a map of attributes -> Objects where each object may either be a simple string, int,

 * etc or another map.<p>

 * So. for example $username$ is accessible from the template of the model contains a String under

 * the value "username" and $strings.hello$ is accessible if the model contains a map under the key

 * "strings" and this map contains an attribute under the key "hello"

 *

 * @author Ran Tavory (ran@outbrain.com)

 *

 */

public interface StringTemplateAction {

 

  /**

   * Get the root of the display model.

   *

   * The display model is a map of attributes interpolated by StringTemplate at runtime by

   * substituting the $values$ in the template source.

   * For example to replace $user$ with Ran add

   * <code>map.put("user", "Ran");</code>

   * @return A map of attributes used by StringTemplate to replace in the template.

   */

  public Map<String, Object> getModel();

}

package org.apache.struts2.dispatcher;

 

import java.io.IOException;

import java.io.InputStream;

import java.io.Writer;

import java.util.Enumeration;

import java.util.HashMap;

import java.util.Locale;

import java.util.Map;

import java.util.Properties;

 

import javax.servlet.ServletContext;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

import javax.servlet.http.HttpSession;

 

import org.antlr.stringtemplate.NoIndentWriter;

import org.antlr.stringtemplate.StringTemplate;

import org.antlr.stringtemplate.StringTemplateGroup;

import org.antlr.stringtemplate.StringTemplateWriter;

import org.apache.commons.logging.Log;

import org.apache.commons.logging.LogFactory;

import org.apache.struts2.ServletActionContext;

import org.apache.struts2.StrutsConstants;

import org.apache.struts2.views.util.ContextUtil;

import org.apache.struts2.views.util.ResourceUtil;

 

import com.opensymphony.xwork2.ActionInvocation;

import com.opensymphony.xwork2.LocaleProvider;

import com.opensymphony.xwork2.inject.Inject;

import com.opensymphony.xwork2.util.ValueStack;

 

import freemarker.template.Configuration;

import freemarker.template.TemplateException;

 

/**

 * Defines the StringTemplate result type for struts2.

 * <p>

 * To use this result type add the following configuration to your struts.xml:

 * <p>

 * <code>

 * &lt;result-types&gt;<p>

 *   &lt;result-type name="stringtemplate"

 *       class="org.apache.struts2.dispatcher.StringTemplateResult"/&gt;<p>

 * &lt;/result-types&gt;<p>

 *</code>

 *

 * Template files should be located relative to /WEB-INF/st/, so for example a common layout

 * would be:

 * <ul>

 * <li>/WEB-INF/st/pages/page1.st</li>

 * <li>/WEB-INF/st/layouts/layout1.st</li>

 * <li>/WEB-INF/st/snippets/header.st</li>

 * <li>/WEB-INF/st/snippets/footer.st</li>

 * </ul>

 *

 * So page1.st would look like:<p>

 * <code>

 * $layouts/layout1(header=snippets/header(), footer=snippets/footer())$

 * </code>

 * <p>

 * And the associated struts action would be:

 * <p>

 * <code>

 * &lt;action name="page1"

 *      class="com.mycompany.Page1Action"&gt; <p>

 *        &lt;result name="success"

 *          type="stringtemplate"&gt;/WEB-INF/st/pages/page1.st&lt;/result&gt;<p>

 * &lt;/action&gt;

 * </code>

 *<p>

 * Localization:

 * All string property files should be packed into the classpath (in one of the jars) at

 * /lang/.<p>

 * For example: <p>

 * <ul>

 * <li>/lang/en_US.properties</li>

 * <li>/lang/fr_FR.properties</li>

 * </ul>

 *

 * @author Ran Tavory (ran@outbrain.com)

 *

 */

public class StringTemplateResult extends StrutsResultSupport {

 

  /**

   * Base path of StringTemplate template files.

   * This is usually something like /WEB-INF/ or /WEB-INF/st/.

   * Must end with /.

   * All templates should reside in subdirectories of this base path and when referencing

   * each other the reference point should be relative to this path.

   * For example, if you have /WEB-INF/pages/page1.st and /WEB-INF/layouts/layout1.st, and assuming

   * the base path is at /WEB-INF then page1 is pages/page1 and layout1 is layouts/layout1.

   * To reference layout1 from page1: $layouts/layout1()$

   */

  public static final String TEMPLATES_BASE =

      "/WEB-INF/st/";

 

  /**

   * Path to the language resource files within the classpath.

   *

   * Resource should be packed inside a jar under /lang/.

   * For example: /lang/en_US.properties

   */

  public static final String LANG_RESOURCE_BASE = "/lang/";

 

  /**

   * If there was an exception during execution it's accessible via $exception$

   */

  public static final String KEY_EXCEPTION = "exception";

 

  /**

   * Session values are accessible via $session.key$

   */

  public static final String KEY_SESSION = "session";

 

  /**

   * All localized strings are accessible via $strings.string_key$

   */

  public static final String KEY_STRINGS = "strings";

 

  /**

   * Request parameters are accessible via $params.key$

   */

  public static final String KEY_REQUEST_PARAMETERS = "params";

 

  /**

   * Request attributes are accessible via $request.attribute$

   */

  public static final String KEY_REQUEST = "request";

 

  private static final Log log = LogFactory.getLog(StringTemplateResult.class);

 

  private static final long serialVersionUID = -2390940981629097944L;