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.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
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.MalformedURLException;
import java.net.URL;
import java.nio.charset.Charset;
import java.util.Hashtable;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import com.x5.template.filters.ChunkFilter;
import com.x5.template.filters.RegexFilter;

// 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 * @version 1.5 */ public class TemplateSet implements ContentSource, ChunkFactory { public static String DEFAULT_TAG_START = "{$"; public static String DEFAULT_TAG_END = "}"; public static final String MACRO_START = "{*"; public static final String MACRO_NAME_END = "}"; public static final String MACRO_END = "{*}"; public static final String MACRO_LET = "{="; public static final String MACRO_LET_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 static final String SUB_START = "{#"; private static final String SUB_NAME_END = "}"; private static final String SUB_END = "{#}"; private static final String COMMENT_START = "{!--"; private static final String COMMENT_END = "--}"; private static final String SKIP_BLANK_LINE = ""; public static final String LITERAL_START = "{^literal}"; public static final String LITERAL_START2 = "{.literal}"; public static final String LITERAL_SHORTHAND = "{^^}"; // this was a dumb idea public static final String LITERAL_END = "{^}"; public static final String LITERAL_END_EXPANDED = "{~.}"; public static final String LITERAL_END_LONGHAND = "{/literal}"; private static final int DEFAULT_REFRESH = 15; // minutes private static final String DEFAULT_EXTENSION = "chtml"; private Hashtable cache = new Hashtable(); private Hashtable cacheFetch = new Hashtable(); private int dirtyInterval = DEFAULT_REFRESH; // minutes private String defaultExtension = DEFAULT_EXTENSION; private String tagStart = DEFAULT_TAG_START; private String tagEnd = DEFAULT_TAG_END; 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 = getDefaultEncoding(); public TemplateSet() {} /** * Makes a template "factory" which reads in template files from the * file system in the templatePath folder. Caches for 15 minutes. * Assumes .html is default file extension. * @param templatePath folder where template files are located. */ public TemplateSet(String templatePath) { this(templatePath, DEFAULT_EXTENSION, DEFAULT_REFRESH); } /** * 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 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 templatePath, String extension, int refreshMins) { if (templatePath != null) { // ensure trailing fileseparator char lastChar = templatePath.charAt(templatePath.length()-1); char fs = System.getProperty("file.separator").charAt(0); if (lastChar != '\\' && lastChar != '/' && lastChar != fs) { templatePath += fs; } this.templatePath = templatePath; } this.dirtyInterval = refreshMins; this.defaultExtension = (extension == null ? DEFAULT_EXTENSION : 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) { 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 Snippet _get(String name, String extension, boolean prettyFail) { //long now = System.currentTimeMillis(); 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 = 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); InputStreamReader reader = new InputStreamReader(in,expectedEncoding ); BufferedReader brTemp = new BufferedReader(reader); readAndCacheTemplate(stub,extension,brTemp); 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) { BufferedReader brTemp = new BufferedReader(new InputStreamReader(inJar,expectedEncoding)); readAndCacheTemplate(stub,extension,brTemp); inJar.close(); template = getFromCache(name, extension); } } } 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 = peekInsideJar("jar:"+jarURL.toString(), resourcePath); if (in != null) return in; } } } } //Set urls = new java.util.HashSet(); //for (Object urlString : ctx.getResourcePaths("/WEB-INF/lib")) { // try { urls.add(ctx.getResource((String) urlString)); } // catch (MalformedURLException e) { /* silent-fail */ } //} } 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; } // // boy, this subtemplate code sure is ugly // ...but being able to define multiple templates per file sure is handy // protected StringBuilder readAndCacheTemplate(String name, String extension, BufferedReader brTemp) throws IOException { StringBuilder sbTemp = new StringBuilder(); StringBuilder commentBuf; String line = null; while (brTemp.ready()) { line = brTemp.readLine(); if (line == null) break; int comPos = line.indexOf(COMMENT_START); int subPos = line.indexOf(SUB_START); // first, skip over any comments boolean killedComment = false; while (comPos > -1 && (subPos < 0 || subPos > comPos)) { //line = stripComment(comPos,line,brTemp); commentBuf = new StringBuilder(); line = skipComment(comPos,line,brTemp,commentBuf); // line up to comPos is beforeComment // line after comPos is now afterComment String beforeComment = line.substring(0,comPos); String afterComment = line.substring(comPos); // preserve comment for clean fetch sbTemp.append(beforeComment); sbTemp.append(commentBuf); line = afterComment; // check for another comment on same line comPos = line.indexOf(COMMENT_START); subPos = line.indexOf(SUB_START); killedComment = true; } // then, strip out any subtemplates if (subPos > -1) { int subEndPos = line.indexOf(SUB_END); if (subEndPos == subPos) { // errant subtemplate end marker, ignore } else { // parse out new template name and begin recursive separation of subtemplates int subNameEnd = line.indexOf(SUB_NAME_END, subPos + SUB_START.length()); if (subNameEnd > -1) { sbTemp.append(line.substring(0,subPos)); String subName = line.substring(subPos + SUB_START.length(),subNameEnd); String restOfLine = line.substring(subNameEnd + SUB_NAME_END.length()); line = readAndCacheSubTemplate(name + "." + subName, extension, brTemp, restOfLine); // if after removing subtemplate, line is blank, don't output a blank line if (line.length() < 1) continue; } } } sbTemp.append(line); // This will not add newline at EOF even if present in template file // Is this the Right Thing to do? // And if not, how would we even detect a trailing newline in the file? // ie since readLine() returns the same string either way. if (brTemp.ready()) sbTemp.append("\n"); } addToCache(name,extension,sbTemp); return sbTemp; } private String getCommentLines(int comBegin, String firstLine, BufferedReader brTemp, StringBuilder sbTemp) throws IOException { int comEnd = firstLine.indexOf(COMMENT_END,comBegin+2); int endMarkerLen = COMMENT_END.length(); if (comEnd > -1) { // easy case -- comment does not span lines comEnd += endMarkerLen; sbTemp.append(firstLine.substring(0,comEnd)); return firstLine.substring(comEnd); } else { sbTemp.append(firstLine); sbTemp.append("\n"); // multi-line comment, keep appending until we encounter comment-end marker String line = null; while (brTemp.ready()) { line = brTemp.readLine(); if (line == null) break; comEnd = line.indexOf(COMMENT_END); if (comEnd > -1) { comEnd += endMarkerLen; sbTemp.append(line.substring(0,comEnd)); return line.substring(comEnd); } sbTemp.append(line); sbTemp.append("\n"); } // never found! appended rest of file as unterminated comment. burp. return ""; } } private String getLiteralLines(int litBegin, String firstLine, BufferedReader brTemp, StringBuilder sbTemp) throws IOException { int litEnd = firstLine.indexOf(LITERAL_END,litBegin+2); int endMarkerLen = LITERAL_END.length(); // {^} ends a literal block, OR {/literal} -- whichever comes first. int litEndLong = firstLine.indexOf(LITERAL_END_LONGHAND,litBegin+2); if (litEndLong > -1 && (litEnd < 0 || litEndLong < litEnd)) { litEnd = litEndLong; endMarkerLen = TemplateSet.LITERAL_END_LONGHAND.length(); } if (litEnd > -1) { // easy case -- literal does not span lines litEnd += endMarkerLen; sbTemp.append(firstLine.substring(0,litEnd)); return firstLine.substring(litEnd); } else { sbTemp.append(firstLine); sbTemp.append("\n"); // multi-line literal, keep appending until we encounter literal-end marker String line = null; while (brTemp.ready()) { line = brTemp.readLine(); if (line == null) break; litEnd = line.indexOf(LITERAL_END); // {^} ends a literal block, OR {/literal} -- whichever comes first. litEndLong = line.indexOf(LITERAL_END_LONGHAND); if (litEndLong > -1 && (litEnd < 0 || litEndLong < litEnd)) { litEnd = litEndLong; endMarkerLen = TemplateSet.LITERAL_END_LONGHAND.length(); } if (litEnd > -1) { litEnd += endMarkerLen; sbTemp.append(line.substring(0,litEnd)); return line.substring(litEnd); } sbTemp.append(line); sbTemp.append("\n"); } // never found! appended rest of file as unterminated literal. burp. return ""; } } // locate end of comment, and strip it but save it! private String skipComment(int comPos, String firstLine, BufferedReader brTemp, StringBuilder commentBuf) throws IOException { String beforeComment = firstLine.substring(0,comPos); int comEndPos = firstLine.indexOf(COMMENT_END); if (comEndPos > -1) { // easy case -- comment does not span lines comEndPos += COMMENT_END.length(); // if removing comment leaves line with only whitespace... commentBuf.append(firstLine.substring(comPos,comEndPos)); return beforeComment + firstLine.substring(comEndPos); } else { // keep eating lines until the end marker is found commentBuf.append(firstLine.substring(comPos)); commentBuf.append("\n"); String line = null; while (brTemp.ready()) { line = brTemp.readLine(); if (line == null) break; comEndPos = line.indexOf(COMMENT_END); if (comEndPos > -1) { comEndPos += COMMENT_END.length(); commentBuf.append(line.substring(0,comEndPos)); return beforeComment + line.substring(comEndPos); } else { commentBuf.append(line); commentBuf.append("\n"); } } // never found! ate rest of file. burp. return beforeComment; } } // locate end of comment and remove it from input private String stripComment(int comPos, String firstLine, BufferedReader brTemp) throws IOException { String beforeComment = firstLine.substring(0,comPos); int comEndPos = firstLine.indexOf(COMMENT_END); if (comEndPos > -1) { // easy case -- comment does not span lines comEndPos += COMMENT_END.length(); // if removing comment leaves line with only whitespace... return beforeComment + firstLine.substring(comEndPos); } else { // keep eating lines until the end marker is found String line = null; while (brTemp.ready()) { line = brTemp.readLine(); if (line == null) break; comEndPos = line.indexOf(COMMENT_END); if (comEndPos > -1) { comEndPos += COMMENT_END.length(); return beforeComment + line.substring(comEndPos); } } // never found! ate rest of file. burp. return beforeComment; } } public static int findLiteralMarker(String text) { return findLiteralMarker(text, 0); } public static int findLiteralMarker(String text, int startAt) { int literalPos = text.indexOf(LITERAL_START, startAt); int litPos = text.indexOf(LITERAL_SHORTHAND, startAt); if (litPos > -1 && literalPos > -1) { literalPos = Math.min(literalPos, litPos); } else if (litPos > -1 || literalPos > -1) { literalPos = Math.max(literalPos, litPos); } return literalPos; } // scan until matching end-of-subtemplate marker found {#} // recurse for stripping/caching nested subtemplates // strip out all comments // preserve {^literal}...{^} blocks (also {^^}...{^} ) private String readAndCacheSubTemplate(String name, String extension, BufferedReader brTemp, String firstLine) throws IOException { StringBuilder sbTemp = new StringBuilder(); // scan for markers int subEndPos = firstLine.indexOf(SUB_END); int comPos = firstLine.indexOf(COMMENT_START); int literalPos = findLiteralMarker(firstLine); boolean skipFirstLine = false; // special handling for literal blocks & comments while (literalPos > -1 || comPos > -1) { // if end-marker present, kick out if it's not inside a comment or a literal block if (subEndPos > -1) { if ((literalPos < 0 || subEndPos < literalPos) && (comPos < 0 || subEndPos < comPos)) { break; } } // first, preserve any literal blocks while (literalPos > -1 && (comPos < 0 || comPos > literalPos)) { if (subEndPos < 0 || subEndPos > literalPos) { firstLine = getLiteralLines(literalPos, firstLine, brTemp, sbTemp); // skipped literal block. re-scan for markers. comPos = firstLine.indexOf(COMMENT_START); subEndPos = firstLine.indexOf(SUB_END); literalPos = findLiteralMarker(firstLine); } else { break; } } // next, strip out any comments while (comPos > -1 && (subEndPos < 0 || subEndPos > comPos) && (literalPos < 0 || literalPos > comPos)) { int lenBefore = firstLine.length(); firstLine = stripComment(comPos,firstLine,brTemp); int lenAfter = firstLine.length(); if (lenBefore != lenAfter && firstLine.trim().length() == 0) { skipFirstLine = true; } // stripped comment lines. re-scan for markers. comPos = firstLine.indexOf(COMMENT_START); subEndPos = firstLine.indexOf(SUB_END); literalPos = findLiteralMarker(firstLine); } } // keep reading lines until we encounter end marker if (subEndPos > -1) { // aha, the subtemplate ends on this line. sbTemp.append(firstLine.substring(0,subEndPos)); addToCache(name,extension,sbTemp); return firstLine.substring(subEndPos+SUB_END.length()); } else { // subtemplate not finished, keep going if (!skipFirstLine) { sbTemp.append(firstLine); if (brTemp.ready() && firstLine.length() > 0) sbTemp.append("\n"); } while (brTemp.ready()) { try { String line = getNextTemplateLine(name, extension, brTemp, sbTemp); if (line == null) break; if (line == SKIP_BLANK_LINE) continue; sbTemp.append(line); if (brTemp.ready()) sbTemp.append("\n"); } catch (EndOfSnippetException e) { addToCache(name,extension,sbTemp); return e.getRestOfLine(); } } // end of file but with no matching SUB_END? -- wrap it up... addToCache(name,extension,sbTemp); return ""; } } private String getNextTemplateLine(String name, String extension, BufferedReader brTemp, StringBuilder sbTemp) throws IOException, EndOfSnippetException { String line = brTemp.readLine(); if (line == null) return null; int comPos = line.indexOf(COMMENT_START); int subPos = line.indexOf(SUB_START); int subEndPos = line.indexOf(SUB_END); int litPos = findLiteralMarker(line); // special handling for literal blocks & comments while (litPos > -1 || comPos > -1) { // if end-marker present, kick out if it's not inside a comment or a literal block if (subEndPos > -1) { if ((litPos < 0 || subEndPos < litPos) && (comPos < 0 || comPos < subEndPos)) { break; } } // if start-marker present, kick out if it's not inside a comment or a literal block if (subPos > -1) { if ((litPos < 0 || subPos < litPos) && (comPos < 0 || comPos < subPos)) { break; } } // first, preserve any literal blocks while (litPos > -1 && (comPos < 0 || comPos > litPos)) { if (subEndPos < 0 || subEndPos > litPos) { line = getLiteralLines(litPos, line, brTemp, sbTemp); // skipped literal block. re-scan for markers. comPos = line.indexOf(COMMENT_START); subPos = line.indexOf(SUB_START); subEndPos = line.indexOf(SUB_END); litPos = findLiteralMarker(line); } else { break; } } // next, skip over any comments while (comPos > -1 && (subPos < 0 || subPos > comPos) && (subEndPos < 0 || subEndPos > comPos) && (litPos < 0 || litPos > comPos)) { /* int lenBefore = line.length(); line = stripComment(comPos,line,brTemp); int lenAfter = line.length(); if (lenBefore != lenAfter && line.trim().length() == 0) { // if after removing comment, line is blank, don't output a blank line return SKIP_BLANK_LINE; }*/ // new plan -- preserve comments, let Snippet strip them out line = getCommentLines(comPos, line, brTemp, sbTemp); // re-scan for markers comPos = line.indexOf(COMMENT_START); subPos = line.indexOf(SUB_START); subEndPos = line.indexOf(SUB_END); litPos = findLiteralMarker(line); } } // keep reading lines until end marker if (subPos > -1 || subEndPos > -1) { if (subEndPos > -1 && (subPos == -1 || subEndPos <= subPos)) { // wrap it up sbTemp.append(line.substring(0,subEndPos)); throw new EndOfSnippetException(line.substring(subEndPos+SUB_END.length())); } else if (subPos > -1) { int subNameEnd = line.indexOf(SUB_NAME_END, subPos + SUB_START.length()); if (subNameEnd > -1) { sbTemp.append(line.substring(0,subPos)); String subName = line.substring(subPos + SUB_START.length(),subNameEnd); String restOfLine = line.substring(subNameEnd + SUB_NAME_END.length()); line = readAndCacheSubTemplate(name + "." + subName, extension, brTemp, restOfLine); // if after removing subtemplate, line is blank, don't output a blank line if (line.length() < 1) return SKIP_BLANK_LINE; } } } return line; } private void addToCache(String name, String extension, StringBuilder template) { String ref = extension + "." + name; // first, store raw template in case it is needed String cleanRef = "_CLEAN_:"+ref; cache.put(cleanRef, Snippet.makeLiteralSnippet(template.toString()) ); cacheFetch.put(cleanRef, new Long(System.currentTimeMillis()) ); // then, pre-process a bit for speed gain template = expandShorthand(name,template); if (template == null) return; String tpl = removeBlockTagIndents(template.toString()); cache.put(ref, Snippet.getSnippet(tpl)); cacheFetch.put(ref, new Long(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 (~cond =~ /\/x{1,3}/)} but it's already nigh-unreadable... return RegexFilter.applyRegex(template, "s/^[ \\t]*(\\{(\\^|\\~\\.)\\/?(loop|if|else|elseIf|divider|onEmpty|body)([^\\}]*|[^\\}]*\\/[^\\/]*\\/[^\\}]*)\\})[ \\t]*$/$1/gm"); } public static StringBuilder expandShorthand(String name, StringBuilder template) { // do NOT place in cache if ^super directive is found // that way, the parent layer will be used instead. if (template.indexOf("{^super}") > -1 || template.indexOf("{.super}") > -1) return null; // to allow shorthand intra-template references, must pre-process the template // at this point and expand any intra-template references, eg: // {~.includeIf(...).#xxx} => {~.includeIf(...).template_name#xxx} // // Hmm, refs that start with a hash should always be toplevel! // so {#subtemplate}...{~.includeIf(...).#xxx} ...{#} // is a reference to template_name#xxx NOT a nested sub like template_name#subtemplate#xxx // // might not be worth it, would have to track down refs inside onmatch and ondefined filters // although it could be more efficient to expand shorthand syntax at this stage: // {+#sub} => {~.include.template_name#sub} // {+(cond)#sub} => {~.includeIf(cond).template_name#sub} // // and you'd have to catch stuff like this... // {~asdf|onmatch(/xyz/,+#xyz,/abc/,+#abc)nomatch(+#def)} // => {~asdf|onmatch(/xyz/,~.include.template_name#xyz,/abc/,~.include.template_name#abc)nomatch(~.include.template_name#def)} // // or even just default values a la (I don't even remember, is this supported?) // {~asdf:+#def} => {~asdf:+template_name#def} => {~asdf:~.include.template_name#def} // determine what shorthand refs should expand into // (template filename is everything up to the first dot) String fullRef = name; if (fullRef != null) { int dotPos = fullRef.indexOf('.'); if (dotPos > 0) fullRef = name.substring(0,dotPos); } // restrict search to inside tags int cursor = template.indexOf("{"); while (cursor > -1) { if (template.length() == cursor+1) return template; // kick out at first sign of trouble char afterBrace = template.charAt(cursor+1); if (afterBrace == '+') { cursor = expandShorthandInclude(template,fullRef,cursor); } else if (afterBrace == '~' || afterBrace == '$') { cursor = expandShorthandTag(template,fullRef,cursor); } else if (afterBrace == '^' || afterBrace == '.') { // check for literal block, and do not perform expansions // inside any literal blocks. int afterLiteralBlock = skipLiterals(template,cursor); if (afterLiteralBlock == cursor) { // ^ is shorthand for ~. eg {^include.#xyz} or {^wiki.External_Content} template.replace(cursor+1,cursor+2,"~."); // re-process, do not advance cursor. } else { cursor = afterLiteralBlock; } } else if (afterBrace == '/') { // {/ is short for {^/ which is short for {~./ template.replace(cursor+1,cursor+2,"~./"); // re-process, do not advance cursor. } else if (afterBrace == '*') { cursor = expandShorthandMacro(template,fullRef,cursor); } else { cursor += 2; } // on to the next tag... if (cursor > -1) cursor = template.indexOf("{",cursor); } return template; } private static int skipLiterals(StringBuilder template, int cursor) { int wall = template.length(); int shortLen = LITERAL_SHORTHAND.length(); int scanStart = cursor; if (cursor + shortLen <= wall && template.substring(cursor,cursor+shortLen).equals(LITERAL_SHORTHAND)) { scanStart = cursor + shortLen; } else { int longLen = LITERAL_START2.length(); if (cursor + longLen <= wall && template.substring(cursor,cursor+longLen).equals(LITERAL_START2)) { scanStart = cursor + longLen; } else { longLen = LITERAL_START.length(); if (cursor + longLen <= wall && template.substring(cursor,cursor+longLen).equals(LITERAL_START)) { scanStart = cursor + longLen; } } } if (scanStart > cursor) { // found a literal-block start marker. scan for the matching end-marker. int tail = template.indexOf(LITERAL_END, scanStart); int longTail = template.indexOf(LITERAL_END_LONGHAND, scanStart); tail = (tail < 0) ? longTail : (longTail < 0) ? tail : Math.min(tail, longTail); if (tail < 0) { return wall; } else { return tail + (tail == longTail ? LITERAL_END_LONGHAND.length() : LITERAL_END.length()); } } else { return cursor; } } private static int expandFnArgs(StringBuilder template, String fullRef, int cursor, String fnCall, int tagEnd) { int tagCursor = 0; StringBuilder expanded = null; int hashPos = fnCall.indexOf("#"); // let's just (lame but works) assume that all hashes after a // delimiter are hashrefs that need to be expanded while (hashPos > 1) { char preH = fnCall.charAt(hashPos-1); if (preH == '"' || preH == ',' || preH == ' ' || preH == '(') { if (expanded == null) expanded = new StringBuilder(); // everything new up to now is certified "clean" expanded.append(fnCall.substring(tagCursor,hashPos)); // pop in the base template ref expanded.append(fullRef); tagCursor = hashPos; } hashPos = fnCall.indexOf("#",hashPos+1); } if (expanded != null) { // grab the tail expanded.append(fnCall.substring(tagCursor)); String expandedTag = expanded.toString(); // insert tag, now with fully-qualified refs back into template template.replace(cursor+2,tagEnd,expandedTag); // update tagEnd to reflect added chars tagEnd += (expandedTag.length() - fnCall.length()); } cursor = tagEnd+1; return cursor; } private static int expandShorthandMacro(StringBuilder template, String fullRef, int cursor) { int offset = 2; while (template.charAt(cursor+offset) == ' ') offset++; if (template.charAt(cursor+offset) == '#') { template.insert(cursor+offset,fullRef); int macroMarkerEnd = template.indexOf(MACRO_NAME_END,cursor+offset+fullRef.length()+1); if (macroMarkerEnd < 0) return cursor+1; return macroMarkerEnd + MACRO_NAME_END.length(); } int macroMarkerEnd = template.indexOf(MACRO_NAME_END,cursor+offset); if (macroMarkerEnd < 0) return cursor+1; return macroMarkerEnd + MACRO_NAME_END.length(); } private static int expandShorthandTag(StringBuilder template, String fullRef, int cursor) { int tagEnd = nextUnescapedDelim("}",template,cursor+2); if (tagEnd < 0) return -1; // kick out at any sign of trouble // so, this is lame but 99.999% of the time the following strings // inside a tag body can be expanded correctly without regard to context: // // ,+# => ,~.include.xxx# - inside onmatch // :+# => :~.include.xxx# - ifnull-include // (+# => (~.include.xxx# - inside nomatch/ondefined // ).# => ).xxx# => - long includeIf(...) syntax // ~.include.# => ~.include.xxx# - long include syntax // // where xxx is the fully qualified template reference // // not the most efficient, but fast enough // String tagDirective = template.substring(cursor+2,tagEnd); // // shorthand refs in fnCall args like ^loop(...) and ^grid(...) // will present a little differently. // if (tagDirective.startsWith(".loop") || tagDirective.startsWith(".grid")) { return expandFnArgs(template, fullRef, cursor, tagDirective, tagEnd); } int tagCursor = 0; StringBuilder expanded = null; int hashPos = tagDirective.indexOf("#"); while (hashPos > 1) { char a = tagDirective.charAt(hashPos-2); char b = tagDirective.charAt(hashPos-1); if (b == '+') { if (a == ',' || a == ':' || a == '(') { if (expanded == null) expanded = new StringBuilder(); expanded.append(tagDirective.substring(tagCursor,hashPos-1)); expanded.append("~.include."); expanded.append(fullRef); tagCursor = hashPos; } } else if ((a == ')' || a == 'e' || a == 'c') && (b == '.' || b == ' ')) { // e for include, c for exec if (expanded == null) expanded = new StringBuilder(); expanded.append(tagDirective.substring(tagCursor,hashPos)); expanded.append(fullRef); tagCursor = hashPos; } else if (b == '(' && a == 'r') { // {$tag|filter(#ref)} if (expanded == null) expanded = new StringBuilder(); expanded.append(tagDirective.substring(tagCursor,hashPos)); expanded.append(fullRef); tagCursor = hashPos; } hashPos = tagDirective.indexOf("#",hashPos+1); } if (expanded != null) { expanded.append(tagDirective.substring(tagCursor)); String expandedTag = expanded.toString(); template.replace(cursor+2,tagEnd,expandedTag); // update tagEnd to reflect added chars tagEnd += (expandedTag.length() - tagDirective.length()); } cursor = tagEnd+1; return cursor; } private static int expandShorthandInclude(StringBuilder template, String fullRef, int cursor) { if (template.length() == cursor+2) return -1; char afterPlus = template.charAt(cursor+2); if (afterPlus == '#') { // got one, replace + with long include syntax and fully qualified reference template.replace(cursor+1,cursor+2,"~.include."+fullRef); cursor += 11; // skip {~.include. cursor += fullRef.length(); // skip what we just inserted cursor = template.indexOf("}",cursor); } else if (afterPlus == '(') { // scan to end of condition int endCond = nextUnescapedDelim(")",template,cursor+3); if (endCond < 0) return -1; // kick out at any sign of trouble String cond = template.substring(cursor+2,endCond+1); if (template.length() == endCond+1) return -1; if (template.charAt(endCond) == '#') { // got one, replace +(cond) with long includeIf syntax and FQRef String expanded = "~.includeIf"+cond+"."+fullRef; template.replace(cursor+1,endCond+1,expanded); cursor++; // skip { cursor += expanded.length(); cursor = template.indexOf("}",cursor); } } else { // move along, nothing to expand here. cursor += 2; } return cursor; } public static int nextUnescapedDelim(String delim, StringBuilder sb, int searchFrom) { int delimPos = sb.indexOf(delim, searchFrom); boolean isProvenDelimeter = false; while (!isProvenDelimeter) { // count number of backslashes that precede this forward slash int bsCount = 0; while (delimPos-(1+bsCount) >= searchFrom && sb.charAt(delimPos - (1+bsCount)) == '\\') { bsCount++; } // if odd number of backslashes precede this delimiter char, it's escaped // if even number precede, it's not escaped, it's the true delimiter // (because it's preceded by either no backslash or an escaped backslash) if (bsCount % 2 == 0) { isProvenDelimeter = true; } else { // keep looking for real delimiter delimPos = sb.indexOf(delim, delimPos+1); // if the expr is not legal (missing delimiters??), bail out if (delimPos < 0) return -1; } } return delimPos; } 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 = ((Long) cacheFetch.get(ref)).longValue(); // 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; } private String truncateNameToStub(String name) { int slashPos = name.lastIndexOf('/'); if (slashPos < -1) slashPos = name.lastIndexOf('\\'); String folder = null; String stub; if (slashPos > -1) { folder = name.substring(0,slashPos+1); stub = name.substring(slashPos+1).replace('#','.'); } else { stub = name.replace('#','.'); } int dotPos = stub.indexOf("."); if (dotPos > -1) stub = stub.substring(0,dotPos); if (slashPos > -1) { char fs = System.getProperty("file.separator").charAt(0); folder.replace('\\',fs); folder.replace('/',fs); return folder + stub; } else { return stub; } } public String getTemplatePath(String templateName, String ext) { String stub = truncateNameToStub(templateName); return templatePath + stub + '.' + ext; } public String getResourcePath(String templateName, String ext) { String stub = truncateNameToStub(templateName); if (layerName == null) { return "/themes/" + stub + '.' + ext; } else { return "/themes/" + layerName + stub + '.' + ext; } } 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 = layerName; if (layerName != null && !layerName.endsWith("/")) { this.layerName = this.layerName + "/"; } } public void setEncoding(String encoding) { this.expectedEncoding = encoding; } private String getDefaultEncoding() { // can use system env var to specify default encoding // other than UTF-8 String override = System.getProperty("chunk.template.charset"); if (override != null) { if (override.equalsIgnoreCase("SYSTEM")) { // use system default charset return Charset.defaultCharset().toString(); } else { return override; } } else { // default is UTF-8 return "UTF-8"; } } public Map getFilters() { return null; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy