/* vim: set ts=2 et sw=2 cindent fo=qroca: */ package com.globant.katari.core.sitemesh; import java.util.Collections; import java.util.List; import java.util.LinkedList; import java.util.Enumeration; import java.util.Set; import java.util.HashSet; import java.util.Locale; import java.io.File; import java.io.IOException; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang.Validate; import com.globant.katari.core.web.ServletConfigWrapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.MessageSource; import org.springframework.context.MessageSourceResolvable; import org.springframework.context.NoSuchMessageException; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.GenericWebApplicationContext; import org.springframework.web.context.support.WebApplicationContextUtils; import org.springframework.web.servlet.support.RequestContext; import org.springframework.web.servlet.view.AbstractTemplateView; import freemarker.cache.TemplateLoader; import freemarker.cache.MultiTemplateLoader; import freemarker.cache.ClassTemplateLoader; import freemarker.cache.FileTemplateLoader; import freemarker.cache.WebappTemplateLoader; import freemarker.template.SimpleHash; import freemarker.template.Template; import freemarker.template.TemplateModel; /** A reimplementation of the freemarker decorator servlet for sitemesh that * can specify multiple loading paths. * * This servlet adds the following parameters: * * - AdditionalTemplatePaths: The AdditionalTemplatePaths parameter is a comma * separated list of paths following the same syntax as the original * FreemarkerServlet TemplatePath parameter * * - DebugPrefix: a prefix to prepend to the path component of the TemplatePath * to look for ftl templates in the file system. * * - AdditionalDebugPrefixes: This is a comma separated list of prefixes to * prepend to the path component of each of the items in * AdditionalTemplatePaths. * * - debug: The debug parameter enables hot reloading of templates from the * file system. If this is false, DebugPrefix and AdditionalDebugPrefixes are * ignored. * * Finally, this servlet expects, in the constructor, the message source to use * to resolve message codes. As a side effect of this, this servlet cannot be * created in web.xml. */ public class FreemarkerDecoratorServlet extends com.opensymphony.module.sitemesh.freemarker.FreemarkerDecoratorServlet { /** The serial version UID. */ private static final long serialVersionUID = 1L; /** The class logger. */ private static Logger log = LoggerFactory.getLogger( FreemarkerDecoratorServlet.class); /** The position in the url where the resource starts. * * For example, file:///a/b.c: the prefix is 'file://', the resource is * /a/b.c, (starts at position 7). * * class://a/b.c: the prefix is 'class:/', the resource is /a/b.c, (starts at * position 7). */ private static final int RESOURCE_OFFSET = 7; /** The value of the TemplatePath initialization parameter. * * It is used only to add the prefix for debug. It is null in case this * parameter was not specified. */ private String templatePath = null; /** The value of the AdditionalTemplatePaths initialization parameter. * * It is null in case this parameter was not specified. */ private String additionalPaths = null; /** The value of the DebugPrefix initialization parameter. */ private String debugPrefix = null; /** The value of the AdditionalDebugPrefixes initialization parameter. * * It is null in case this parameter was not specified. */ private String additionalPrefixes = null; /** The value of the debug initialization parameter. */ private boolean debug = false; /** The message source to resolve message codes. * * This is never null. */ private MessageSource messageSource; /** The local web application context associated with this filter. * * This is used to scope the message resolution to a specific bean. It is * never null after the call to init. */ private GenericWebApplicationContext applicationContext = null; /** Creates a freemarker decorator filter. * * @param theMessageSource the message source to resolve message codes. It * cannot be null. */ public FreemarkerDecoratorServlet(final MessageSource theMessageSource) { Validate.notNull(theMessageSource, "The message source cannot be null."); messageSource = theMessageSource; } /** A servlet config that strips the DebugPrefix, AdditionalTemplatePaths, * AdditionalDebugPrefixes and debug initialization parameter from the list * of parameters. * * This is due to the fact that adding unrecognized parameters to sitemesh * FreemarkerDecoratorServlet throws an exception. */ private static final class ConfigWithoutAdditionlPaths extends ServletConfigWrapper { /** Init parameters to strip, never null. */ private static Set<String> strippedParameters = new HashSet<String>(); static { strippedParameters.add("AdditionalTemplatePaths"); strippedParameters.add("DebugPrefix"); strippedParameters.add("AdditionalDebugPrefixes"); strippedParameters.add("debug"); } /** Builds a new ConfigWithoutAdditionlPaths. * * @param delegate The original servlet config. */ private ConfigWithoutAdditionlPaths(final ServletConfig delegate) { super(delegate); } /** Returns the names of the servlet's initialization parameters as an * Enumeration of String objects, or an empty Enumeration if the servlet * has no initialization parameters. * * This enumeration does not include AdditionalTemplatePaths, DebugPrefix, * AdditionalDebugPrefixes nor debug. * * @return an enumeration with the names of the parameters. */ @SuppressWarnings("unchecked") public Enumeration getInitParameterNames() { List<String> original = Collections.list( (Enumeration<String>) super.getInitParameterNames()); List<String> modified = new LinkedList<String>(); for (String name : original){ if (!strippedParameters.contains(name)) { modified.add(name); } } return Collections.enumeration(modified); } } /** Initializes the servlet. * * @param config The container provided servlet config. * * @throws ServletException in case of error. */ public void init(final ServletConfig config) throws ServletException { log.trace("Entering init"); // Save the additional paths to be used in init(); templatePath = config.getInitParameter("TemplatePath"); log.debug("Initialized templatePath to {}", templatePath); additionalPaths = config.getInitParameter("AdditionalTemplatePaths"); log.debug("Initialized additionalPaths to {}", additionalPaths); debugPrefix = config.getInitParameter("DebugPrefix"); log.debug("Initialized debugPrefix to {}", debugPrefix); additionalPrefixes = config.getInitParameter("AdditionalDebugPrefixes"); log.debug("Initialized additionalPrefixes to {}", additionalPrefixes); String debugParameter = config.getInitParameter("debug"); if (debugParameter != null) { debug = Boolean.valueOf(debugParameter); } log.debug("Initialized debug to {}", debug); super.init(new ConfigWithoutAdditionlPaths(config)); // Creates the local application context. It delegates all message // resolving to the message passed as parameter. applicationContext = new GenericWebApplicationContext() { public String getMessage(final String code, final Object[] args, final String defaultMessage, final Locale locale) { return messageSource.getMessage(code, args, defaultMessage, locale); } public String getMessage(final String code, final Object[] args, final Locale locale) throws NoSuchMessageException { return messageSource.getMessage(code, args, locale); } public String getMessage(final MessageSourceResolvable resolvable, final Locale locale) throws NoSuchMessageException { return messageSource.getMessage(resolvable, locale); } }; WebApplicationContext parent = WebApplicationContextUtils .getRequiredWebApplicationContext(getServletContext()); applicationContext.setParent(parent); applicationContext.refresh(); log.trace("Leaving init"); } /** Initializes the freemarker configuration. * * Adds the additional template paths to the freemarker configuration. * * @throws ServletException in case of error. */ public void init() throws ServletException { super.init(); getLoaders(); } /** Testing helper. * * This is the init implementation, but returns the list of loaders. This * list is only used in the unit test. * * @return the list of loaders, used only for unit testing. * * @throws ServletException if initialization failed. */ protected TemplateLoader[] getLoaders() throws ServletException { // Use the saved additional paths to modify the freemarker configuration. List<TemplateLoader> loaders = new LinkedList<TemplateLoader>(); // Add the loader for the file-system based TemplatePath. if (debug && debugPrefix != null && templatePath != null) { TemplateLoader loader = createLoader(debugPrefix, templatePath); if (loader != null) { loaders.add(loader); } } // Adds the default loader. loaders.add(getConfiguration().getTemplateLoader()); if (additionalPaths != null) { List<TemplateLoader> additionalLoaders = getAdditionalLoaders( additionalPaths, additionalPrefixes); loaders.addAll(additionalLoaders); } TemplateLoader[] loaderArray; loaderArray = loaders.toArray(new TemplateLoader[loaders.size()]); if (loaders.size() != 0) { MultiTemplateLoader loader = new MultiTemplateLoader(loaderArray); getConfiguration().setTemplateLoader(loader); } return loaderArray; } /** Creates a template loader for a path and a prefix. * * See addPrefix for parameter information. * * @param prefix The prefix to add. it cannot be null. * * @param path The path to add the prefix to. It cannot be null. * * @return a file system relative template loader. If can be null if the * generated path is invalid. An invalid path must be ignored. */ private TemplateLoader createLoader(final String prefix, final String path) { FileTemplateLoader loader = null; String filePath = addPrefix(prefix, path); try { loader = new FileTemplateLoader(new File(filePath)); log.debug("Added '{}' to template loader path.", filePath); } catch (IOException e) { // We ignore this exception. log.warn("Error loading {}.", filePath, e); } return loader; } /** Creates the loaders for the additional template paths. * * @param additionalTemplatePaths a ; separated list of paths of the form * [file|class]://[something], or /[something]. It cannot be null. * * @param additionalDebugPrefixes a ; separated list of prefixes to be * prepended to each of the additionalTemplatePaths. Each new path is added * to the list before the original one, so it takes priority over it. * * @return a list of template loaders, one for each path. Never returns null. * * @throws ServletException if initialization failed. */ private List<TemplateLoader> getAdditionalLoaders(final String additionalTemplatePaths, final String additionalDebugPrefixes) throws ServletException { Validate.notNull(additionalTemplatePaths); String[] paths = additionalPaths.split(";"); String[] prefixes = null; if (debug && additionalDebugPrefixes != null) { prefixes = additionalDebugPrefixes.split(";"); Validate.isTrue(paths.length == prefixes.length && debug, "AdditionalTemplatePaths and AdditionalDebugPrefixes must have the" + " same number of elements."); } List<TemplateLoader> loaders = new LinkedList<TemplateLoader>(); for (int i = 0; i < paths.length; ++i) { String path = paths[i].trim(); Validate.notEmpty(path, "Empty template path. Perhaps a trailing ;?"); log.debug("Analyzing '{}'.", path); if (path.startsWith("class://")) { // Adds the debug loader. if (prefixes != null) { String prefix = prefixes[i].trim(); TemplateLoader loader = createLoader(prefix, path); if (loader != null) { loaders.add(loader); } } // Strips the class:/ String classPath = path.substring(RESOURCE_OFFSET); loaders.add(new ClassTemplateLoader(getClass(), classPath)); log.debug("Added '{}' to template loader path.", classPath); } else if (path.startsWith("file://")) { String filePath = path.substring(RESOURCE_OFFSET); try { loaders.add(new FileTemplateLoader(new File(filePath))); log.debug("Added '{}' to template loader path.", filePath); } catch (IOException e) { throw new ServletException("Loading file " + filePath, e); } } else { loaders.add(new WebappTemplateLoader(getServletContext(), path)); log.debug("Added '{}' to template loader path.", path); } } return loaders; } /** Adds a prefix to a path. * * @param prefix The prefix to add. it cannot be null. * * @param path The path to add the prefix to. This path may be of the form * class://[something], file://[something] or has no :. The prefix is only * added when path starts with class:. It cannot be null. * * @return a file system relative path that will represent the same resource * as the path parameter. The prefix is only applied to a classpath located * resource (class://). Never returns null. */ private String addPrefix(final String prefix, final String path) { Validate.notNull(prefix, "The prefix cannot be null."); Validate.notNull(path, "The path cannot be null."); if (path.trim().startsWith("class://")) { String classPath = path.trim().substring(RESOURCE_OFFSET); return prefix.trim() + classPath; } else { return path; } } /** {@inheritDoc} * * Adds and exposes a RequestContext to the freemarker temeplate decorator, * to support the macros from spring.ftl. */ protected boolean preTemplateProcess(final HttpServletRequest request, final HttpServletResponse response, final Template template, final TemplateModel templateModel) throws ServletException, IOException { request.setAttribute(RequestContext.WEB_APPLICATION_CONTEXT_ATTRIBUTE, applicationContext); SimpleHash model = (SimpleHash) templateModel; model.put(AbstractTemplateView.SPRING_MACRO_REQUEST_CONTEXT_ATTRIBUTE, new RequestContext(request, response, getServletContext(), null)); return super.preTemplateProcess(request, response, template, templateModel); } }