All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.x5.template.TemplateSet Maven / Gradle / Ivy

There is a newer version: 3.6.2
Show newest version
package com.x5.template;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Hashtable;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import com.x5.template.filters.ChunkFilter;
import com.x5.template.filters.RegexFilter;
import com.x5.util.JarResource;
import com.x5.util.Path;

// Project Title: Chunk
// Description: Template Util
// Copyright: Copyright (c) 2007
// Author: Tom McClure

/**
 * TemplateSet is a Chunk "factory" and an easy way to parse
 * template files into Strings.  The default caching behavior is
 * great for high traffic applications.
 *
 * 
 * // Dynamic content in templates is marked with {~...}
 * //
 * // Previously the syntax was {$...} but then I had a project where
 * // some of the templates were shared by a perl script and I got
 * // tired of escaping the $ signs in inline templates in my perl code.
 * //
 * // Before that there was an escaped-HTML-inspired syntax which
 * // looked like &tag_...; since this was thought to be most
 * // compatible with HTML editors like DreamWeaver but it was hard
 * // to read and as it turned out DreamWeaver choked on it.
 * //
 * // See TemplateSet.convertTags(...) and .convertToMyTags(...) for
 * // quick search-and-replace routines for updating tag syntax on
 * // old templates...
 * //
 * // ...Or, if an entire template set uses another syntax just call
 * // .setTagBoundaries("{$", "}") on the TemplateSet object before
 * // you use it to make any chunks.  All subsequent chunks made from
 * // that TemplateSet will find and replace the {$...} style tags.
 * //
 * // Be careful: for interoperability you will need to call
 * // .setTagBoundaries() individually on any blank chunks you make
 * // without the aid of the TemplateSet object, ie with the Chunk
 * // constructor -- better to use the no-arg .makeChunk() method of
 * // the TemplateSet instead, since that automatically coerces the
 * // blank Chunk's tag boundaries correctly.
 * //
 * ///// In summary, for back-compatibility:
 * //
 * // TemplateSet templates = new TemplateSet(...);
 * // templates.setTagBoundaries("{$", "}");
 * // ...
 * //
 * // *** (A) BAD ***
 * // ...
 * // Chunk c = new Chunk(); // will only explode tags like default {~...}
 * //
 * // *** (B) NOT AS BAD ***
 * // ...
 * // Chunk c = new Chunk();
 * // c.setTagBoundaries("{$", "}"); // manually match tag format :(
 * //
 * // *** (C) BEST ***
 * // ...
 * // Chunk c = templates.makeChunk(); // inherits TemplateSet's tag format :)
 * //
 * 
* * Copyright: waived, free to use
* Company: X5 Software
* Updates: Chunk Documentation
* * @author Tom McClure */ public class TemplateSet implements ContentSource, ChunkFactory { public static String DEFAULT_TAG_START = "{$"; public static String DEFAULT_TAG_END = "}"; public static final String INCLUDE_SHORTHAND = "{+"; public static final String PROTOCOL_SHORTHAND = "{."; // allow {^if}...{/if} and {^loop}...{/loop} by auto-expanding these public static final String BLOCKEND_SHORTHAND = "{/"; public static final String BLOCKEND_LONGHAND = "{~./"; // ie "{^/" private static final long oneMinuteInMillis = 60 * 1000; // having a minimum cache time of five seconds improves // performance by avoiding typical multiple parses of a // file for its subtemplates in a short span of code. private static final long MIN_CACHE = 5 * 1000; private Hashtable cache = new Hashtable(); private Hashtable cacheFetch = new Hashtable(); private int dirtyInterval = 0; // minutes private String defaultExtension = null; private String tagStart = DEFAULT_TAG_START; private String tagEnd = DEFAULT_TAG_END; private String classpathThemesFolder = Path.ensureTrailingPathSeparator("/" + Theme.DEFAULT_THEMES_FOLDER); private String templatePath = System.getProperty("templateset.folder",""); private String layerName = null; private Class classInJar = null; private Object resourceContext = null; private boolean prettyFail = true; private String expectedEncoding = TemplateDoc.getDefaultEncoding(); public TemplateSet() {} /** * Makes a template "factory" which reads in template files from the * file system in the templatePath folder. Caches for refreshMins. * Uses "extensions" for the default file extension (do not include dot). * @param classpathThemesFolder folder where template files are located on classpath. * @param templatePath folder where template files are located. * @param extension appends dot plus this String to a template name stub to find template files. * @param refreshMins returns template from cache unless this many minutes have passed. */ public TemplateSet(String classpathThemesFolder, String templatePath, String extension, int refreshMins) { this(templatePath, extension, refreshMins); this.classpathThemesFolder = Path.ensureTrailingPathSeparator(classpathThemesFolder); } public TemplateSet(String templatePath, String extension, int refreshMins) { this.templatePath = Path.ensureTrailingFileSeparator(templatePath); this.dirtyInterval = refreshMins; this.defaultExtension = extension; } /** * Retrieve as String the template specified by name. * If name contains one or more dots it is assumed that the template * definition is nested inside another template. Everything up to the * first dot is part of the filename (appends the DEFAULT extension to * find the file) and everything after refers to a location within the * file where the template contents are defined. *

* For example: String myTemplate = templateSet.get("outer_file.inner_template"); *

* will look for {#inner_template}bla bla bla{#} inside the file * "outer_file.html" or "outer_file.xml" ie whatever your TemplateSet extension is. * @param name the location of the template definition. * @return the template definition from the file as a String */ public Snippet getSnippet(String name) { if (name.charAt(0) == ';') { int nextSemi = name.indexOf(';',1); if (nextSemi < 0) { // missing delimiter return getSnippet(name, defaultExtension); } else { String tpl = name.substring(nextSemi+1); String ext = name.substring(1,nextSemi); return getSnippet(tpl, ext); } } else { return getSnippet(name, defaultExtension); } } public String fetch(String name) { Snippet s = getCleanTemplate(name); if (s == null) return null; // otherwise... return s.toString(); } public String getProtocol() { return "include"; } private Snippet getCleanTemplate(String name) { return getSnippet(name, "_CLEAN_:"+defaultExtension); } /** * Retrieve as String the template specified by name and extension. * If name contains one or more dots it is assumed that the template * definition is nested inside another template. Everything up to the * first dot is part of the filename (appends the PASSED extension to * find the file) and everything after refers to a location within the * file where the template contents are defined. * @param name the location of the template definition. * @param extension the nonstandard extension which forms the template filename. * @return the template definition from the file as a String */ public Snippet getSnippet(String name, String extension) { return _get(name, extension, this.prettyFail); } private void importTemplates(InputStream in, String stub, String extension) throws IOException { TemplateDoc doc = new TemplateDoc(stub, in); for (TemplateDoc.Doclet doclet : doc.parseTemplates(expectedEncoding)) { cacheTemplate(doclet, extension); } } private Snippet _get(String name, String extension, boolean prettyFail) { Snippet template = getFromCache(name, extension); String filename = null; // if not in cache, parse file and place all pieces in cache if (template == null) { String stub = TemplateDoc.truncateNameToStub(name); filename = getTemplatePath(name,extension); char fs = System.getProperty("file.separator").charAt(0); filename = filename.replace('\\',fs); filename = filename.replace('/',fs); try { File templateFile = new File(filename); if (templateFile.exists()) { FileInputStream in = new FileInputStream(templateFile); importTemplates(in, stub, extension); in.close(); template = getFromCache(name, extension); } else { // file does not exist, check around in classpath/jars String resourcePath = getResourcePath(name,extension); InputStream inJar = null; if (classInJar == null) { // theme resource is probably in same // vicinity as calling class. classInJar = grokCallerClass(); } // ideally, somebody called Theme.setJarContext(this.getClass()) // and we have a pointer to the jar where the templates live. if (classInJar != null) { inJar = classInJar.getResourceAsStream(resourcePath); } // last ditch effort, check in surrounding jars in classpath... if (inJar == null) inJar = fishForTemplate(resourcePath); if (inJar != null) { importTemplates(inJar, stub, extension); template = getFromCache(name, extension); inJar.close(); } } } catch (java.io.IOException e) { if (!prettyFail) return null; StringBuilder errmsg = new StringBuilder("[error fetching "); errmsg.append(extension); errmsg.append(" template '"); errmsg.append(name); errmsg.append("']"); template = Snippet.getSnippet(errmsg.toString()); } } if (template == null) { if (prettyFail) { StringBuilder errmsg = new StringBuilder(); errmsg.append("["); errmsg.append(extension); errmsg.append(" template '"); errmsg.append(name); errmsg.append("' not found]"); template = Snippet.getSnippet(errmsg.toString()); } else { return null; } } return template; } // default (package) visibility intentional static Class grokCallerClass() { Throwable t = new Throwable(); StackTraceElement[] stackTrace = t.getStackTrace(); if (stackTrace == null) return null; // calling class is at least four call levels back up the stack trace. // makes an excellent candidate for where to look for theme resources. for (int i=4; i ctxClass = resourceContext.getClass(); Method m = null; try { final Class[] oneString = new Class[]{String.class}; m = ctxClass.getMethod("getResourceAsStream", oneString); if (m != null) { InputStream in = (InputStream)m.invoke(resourceContext, new Object[]{resourcePath}); if (in != null) return in; } // no dice, start peeking inside jars in WEB-INF/lib/ m = ctxClass.getMethod("getResourcePaths", oneString); if (m != null) { Set paths = (Set)m.invoke(resourceContext, new Object[]{"/WEB-INF/lib"}); if (paths != null) { for (Object urlString : paths) { String jar = (String)urlString; if (jar.endsWith(".jar")) { m = ctxClass.getMethod("getResource", oneString); URL jarURL = (URL)m.invoke(resourceContext, new Object[]{jar}); InputStream in = JarResource.peekInsideJar("jar:"+jarURL.toString(), resourcePath); if (in != null) return in; } } } } } catch (SecurityException e) { } catch (NoSuchMethodException e) { } catch (IllegalArgumentException e) { } catch (IllegalAccessException e) { } catch (InvocationTargetException e) { } return null; } /** * Creates a Chunk with no starter template and sets its tag boundary * markers to match the other templates in this set. The Chunk will need * to obtain template pieces via its .append() method. * @return blank Chunk. */ public Chunk makeChunk() { Chunk c = new Chunk(); c.setMacroLibrary(this,this); shareContentSources(c); return c; } /** * Creates a Chunk with a starting template. If templateName contains one * or more dots it is assumed that the template definition is nested inside * another template. Everything up to the first dot is part of the filename * (appends the DEFAULT extension to find the file) and everything after * refers to a location within the file where the template contents are * defined. * * @param templateName the location of the template definition. * @return a Chunk pre-initialized with a snippet of template. */ public Chunk makeChunk(String templateName) { Chunk c = new Chunk(); c.setMacroLibrary(this,this); c.append( getSnippet(templateName) ); shareContentSources(c); return c; } /** * Creates a Chunk with a starting template. If templateName contains one * or more dots it is assumed that the template definition is nested inside * another template. Everything up to the first dot is part of the filename * (appends the PASSED extension to find the file) and everything after * refers to a location within the file where the template contents are * defined. * * @param templateName the location of the template definition. * @param extension the nonstandard extension which forms the template filename. * @return a Chunk pre-initialized with a snippet of template. */ public Chunk makeChunk(String templateName, String extension) { Chunk c = new Chunk(); c.setMacroLibrary(this,this); c.append( getSnippet(templateName, extension) ); shareContentSources(c); return c; } private void cacheTemplate(TemplateDoc.Doclet doclet, String extension) { String name = doclet.getName().replace('#','.'); String ref = extension + "." + name; String cleanRef = "_CLEAN_:" + ref; String template = doclet.getTemplate(); cache.put(cleanRef, Snippet.makeLiteralSnippet(template)); cacheFetch.put(cleanRef, System.currentTimeMillis()); StringBuilder tpl = TemplateDoc.expandShorthand(name,new StringBuilder(template)); if (tpl == null) return; String fastTpl = removeBlockTagIndents(tpl.toString()); cache.put(ref, Snippet.getSnippet(fastTpl, doclet.getOrigin())); cacheFetch.put(ref, System.currentTimeMillis()); } public static String removeBlockTagIndents(String template) { // this regex: s/^\s*({^\/?(...)[^}]*})\s*/$1/g removes leading and trailing whitespace // from lines that only contain {^loop} ... // NB: this regex will not catch {^if (~tag =~ /\/x{1,3}/)} but it's already nigh-unreadable... return RegexFilter.applyRegex(template, "s/^[ \\t]*(\\{(\\% *(\\~\\.)?(end)?|(\\^|\\~\\.)\\/?)(loop|exec|if|else|elseIf|divider|onEmpty|body|data)([^\\}]*|[^\\}]*\\/[^\\/]*\\/[^\\}]*)\\})[ \\t]*$/$1/gmi"); } protected Snippet getFromCache(String name, String extension) { String ref = extension + "." + name.replace('#','.'); Snippet template = null; long cacheHowLong = dirtyInterval * oneMinuteInMillis; if (cacheHowLong < MIN_CACHE) cacheHowLong = MIN_CACHE; if (cache.containsKey(ref)) { long lastFetch = cacheFetch.get(ref); // millis long expireTime = lastFetch + cacheHowLong; if (System.currentTimeMillis() < expireTime) { template = cache.get(ref); } } return template; } /** * Forces subsequent template fetching to re-read the template contents * from the filesystem instead of the cache. */ public void clearCache() { cache.clear(); cacheFetch.clear(); } /** * Controls caching behavior. Set to zero to minimize caching. * @param minutes how long to keep a template in the cache. */ public void setDirtyInterval(int minutes) { dirtyInterval = minutes; } /** * Converts a template with an alternate tag syntax to one that matches * this TemplateSet's tags. * @param withOldTags Template text which contains tags with the old syntax * @param oldTagStart old tag beginning marker * @param oldTagEnd old tag end marker * @return template with tags converted */ public String convertToMyTags(String withOldTags, String oldTagStart, String oldTagEnd) { return convertTags(withOldTags, oldTagStart, oldTagEnd, this.tagStart, this.tagEnd); } /** * Converts a template with an alternate tag syntax to one that matches * the default tag syntax {~myTag}. * @param withOldTags Template text which contains tags with the old syntax * @param oldTagStart old tag beginning marker * @param oldTagEnd old tag end marker * @return template with tags converted */ public static String convertTags(String withOldTags, String oldTagStart, String oldTagEnd) { return convertTags(withOldTags, oldTagStart, oldTagEnd, DEFAULT_TAG_START, DEFAULT_TAG_END); } /** * Converts a template from one tag syntax to another. * @param withOldTags Template text which contains tags with the old syntax * @param oldTagStart old tag beginning marker * @param oldTagEnd old tag end marker * @param newTagStart new tag beginning marker * @param newTagEnd new tag end marker * @return template with tags converted */ public static String convertTags(String withOldTags, String oldTagStart, String oldTagEnd, String newTagStart, String newTagEnd) { StringBuilder converted = new StringBuilder(); int j, k, marker = 0; while ((j = withOldTags.indexOf(oldTagStart,marker)) > -1) { converted.append(withOldTags.substring(marker,j)); marker = j + oldTagStart.length(); if ((k = withOldTags.indexOf(oldTagEnd)) > -1) { converted.append(newTagStart); converted.append(withOldTags.substring(marker,k)); converted.append(newTagEnd); marker = k + oldTagEnd.length(); } else { converted.append(oldTagStart); } } if (marker == 0) { return withOldTags; } else { converted.append(withOldTags.substring(marker)); return converted.toString(); } } public TemplateSet getSubset(String context) { return new TemplateSetSlice(this, context); } // chunk factory now supports sharing content sources with its factory-created chunks private HashSet altSources = null; public void addProtocol(ContentSource src) { if (altSources == null) altSources = new HashSet(); altSources.add(src); } private void shareContentSources(Chunk c) { if (altSources == null) return; java.util.Iterator iter = altSources.iterator(); while (iter.hasNext()) { ContentSource src = iter.next(); c.addProtocol(src); } } public void signalFailureWithNull() { this.prettyFail = false; } public String getTemplatePath(String templateName, String ext) { String stub = TemplateDoc.truncateNameToStub(templateName); String path = templatePath + stub; if (ext != null && ext.length() > 0) { path += '.' + ext; } return path; } public String getResourcePath(String templateName, String ext) { String stub = TemplateDoc.truncateNameToStub(templateName); String path; if (layerName == null) { path = classpathThemesFolder + stub; } else { path = classpathThemesFolder + layerName + stub; } if (ext != null && ext.length() > 0) { path += '.' + ext; } return path; } public String getDefaultExtension() { return this.defaultExtension; } public boolean provides(String itemName) { Snippet found = _get(itemName, defaultExtension, false); if (found == null) { return false; } else { return true; } } public void setJarContext(Class classInSameJar) { this.classInJar = classInSameJar; } public void setJarContext(Object ctx) { // an object with an InputStream getResourceAsStream(String) method this.resourceContext = ctx; } public void setLayerName(String layerName) { this.layerName = Path.ensureTrailingFileSeparator(layerName); } public void setEncoding(String encoding) { this.expectedEncoding = encoding; } public Map getFilters() { return null; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy