/* vim: set ts=2 et sw=2 cindent fo=qroca: */
package com.globant.katari.core.web;
import java.util.List;
import java.util.LinkedList;
import java.io.File;
import java.io.IOException;
import org.apache.commons.lang.Validate;
import freemarker.cache.TemplateLoader;
import freemarker.cache.FileTemplateLoader;
import freemarker.cache.MultiTemplateLoader;
import freemarker.cache.ClassTemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.TemplateException;
import org.springframework.ui.freemarker.SpringTemplateLoader;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** A freemarker configurer that registers a template loader to load the
* "katari.ftl" macro library.
*
* It also supports loading the templates from the file system if debug mode is
* enabled, so that modifications of the template are picked up without having
* to redeploy the application.
*/
public class FreeMarkerConfigurer extends
org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer {
/** The class logger.
*/
private Logger log = LoggerFactory.getLogger(FreeMarkerConfigurer.class);
/** The length of the classpath: string.
*/
private static final int CLASSPATH_PREFIX_LENGTH = 10;
/** Whether debug mode is enabled.
*
* In debug mode, the templates are first search from the file system. This
* makes it possible to edit ftl files and see the result without a redeploy.
*/
private boolean debug = false;
/** A prefix to use to find the resources in the disk as a file.
*
* Only used in debug mode.
*/
// private String debugPrefix = ".";
/** A list of prefixes to use to find the resources in the disk as a file.
*
* Only used in debug mode.
*/
private List<String> debugPrefixes = new LinkedList<String>();
/** Constructor, builds a FreeMarkerConfigurer.
*
* This constructor changes the default preferFileSystemAccess to false.
*
* Spring tries to convert the templateLoaderPath to a file system directory.
* Maven puts the test-classes directory before the classes directory in the
* classpath, so spring resolves templateLoaderPath to the test-classes
* directory if the view directory is found there. But the templates are
* found in the classes directory, so spring is unable to resolve the views.
* preferFileSystemAccess=false forces spring to load templates from the
* classpath, avoiding this problem.
*/
public FreeMarkerConfigurer() {
setPreferFileSystemAccess(false);
}
/** Creates a TemplateLoader that searches for templates in the file system
* and classloader.
*
* @param templateLoaderPath the path to load templates from
*
* @return In debug mode, it returns a MultiTemplateLoader wrapping a file
* system loader and a classpath loader. Otherwise, it simply returns the
* classpath loader.
*/
@Override
protected TemplateLoader getTemplateLoaderForPath(final String
templateLoaderPath) {
log.trace("Entering getTemplateLoaderForPath({})", templateLoaderPath);
TemplateLoader classpathLoader = new
SpringTemplateLoader(getResourceLoader(), templateLoaderPath);
TemplateLoader loader = null;
if (debug) {
String path = templateLoaderPath;
if (path.startsWith("classpath:")) {
path = path.substring(CLASSPATH_PREFIX_LENGTH);
}
if (!path.startsWith("/")) {
// Force a / at the begining.
path = "/" + path;
}
List<TemplateLoader> loaders = new LinkedList<TemplateLoader>();
for (String debugPrefix: debugPrefixes) {
String fileTemplatePath = debugPrefix + path;
log.debug("Debug mode enabled, attempt to load templates from {}",
fileTemplatePath);
try {
loaders.add(new FileTemplateLoader(new File(fileTemplatePath)));
} catch (IOException e) {
// We fall back to the standard loader.
log.debug("Could not find {}, skipping ...", fileTemplatePath);
}
}
// Adds the default classpathLoader
loaders.add(classpathLoader);
loader = new MultiTemplateLoader(loaders.toArray(
new TemplateLoader[loaders.size()]));
} else {
log.debug("Debug mode not enabled, using SpringTemplateLoader");
loader = classpathLoader;
}
Validate.notNull(loader, "Error setting the loader.");
log.trace("Leaving getTemplateLoaderForPath");
return loader;
}
/** Registers an additional ClassTemplateLoader for the katari-provided
* macros.
*
* The new ClassTemplateLoader is added to the end of the list.
*
* @param templateLoaders The current list of template loaders. It cannot be
* null.
*/
@SuppressWarnings("unchecked")
protected void postProcessTemplateLoaders(final List templateLoaders) {
super.postProcessTemplateLoaders(templateLoaders);
templateLoaders.add(new ClassTemplateLoader(
FreeMarkerConfigurer.class, ""));
}
/** Sets the debug mode.
*
* @param debugEnabled true to enable debug mode, false by default.
*/
public void setDebug(final boolean debugEnabled) {
debug = debugEnabled;
}
/** Sets the debug prefix.
*
* This takes precedence over the prefixes set with debugPrefixes.
*
* @param prefix a prefix to add to the template path to look for it in the
* file system. It is a dot by default. A trailing / is removed if present.
* It cannot be null.
*/
public void setDebugPrefix(final String prefix) {
Validate.notNull(prefix, "The prefix cannot be null.");
if (prefix.endsWith("/")) {
debugPrefixes.add(0, prefix.substring(0, prefix.length() - 1));
} else {
debugPrefixes.add(0, prefix);
}
}
/** Sets the debug prefixes.
*
* If both debugPrefix and debugPrefixes is specified, then the template is
* first search for in debugPrefix.
*
* The directories are scanned in order.
*
* @param prefixes a list of prefixes to add to the template path to look for
* templates in the file system. A trailing / is removed if present in any of
* the elements. It cannot be null.
*/
public void setDebugPrefixes(final List<String> prefixes) {
Validate.notNull(prefixes, "The list of debug prefixes cannot be null.");
for (String prefix: prefixes) {
if (prefix.endsWith("/")) {
debugPrefixes.add(prefix.substring(0, prefix.length() - 1));
} else {
debugPrefixes.add(prefix);
}
}
}
/** {@inheritDoc}
*
* Creates a configuration object with the json enabled object wrapper.
*/
@Override
public Configuration createConfiguration() throws IOException,
TemplateException {
Configuration configuration = super.createConfiguration();
configuration.setObjectWrapper(new JsonRepresentationWrapper());
return configuration;
}
}