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

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

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

import java.io.IOException;
import java.io.PrintStream;
import java.io.StringWriter;
import java.io.Writer;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Hashtable;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.Vector;
import java.util.Map;
import java.util.regex.Matcher;

import com.x5.template.filters.Calc;
import com.x5.util.DataCapsule;
import com.x5.util.DataCapsuleReader;
import com.x5.util.ObjectDataMap;
import com.x5.util.TableData;

// Project Title: Chunk
// Description: Template Engine
// Copyright: Waived. Use freely.
// Author: Tom McClure

/**
 * 

* Chunk is part Hashtable, part StringBuilder, part find-and-replace. * *

* Assign an initial template (you can stitch on bits of additional template
* content as you go) with placeholder tags -- eg {$my_tag} -- and then
* set up replacement rules for those tags like so: * *

 *    Theme templates = getTemplates(); // defined elsewhere
 *    Chunk myChunk = templates.makeChunk("my_template");
 *    myChunk.set("my_tag","hello tag");
 *    System.out.print( myChunk.toString() );
 * 
* *

* NB: Template {$tags} are bounded by curly brackets, not (parentheses).
* Be careful to always close off {#sub_templates}bla bla bla{#} with a single
* hash mark surrounded by curly brackets/braces. * *

* Here's an even simpler example, where the template string is supplied
* without using a theme: * *

 *    String templateBody = "Hello {$name}!  Your balance is ${$balance}."
 *         + "Pleasure serving you, {$name}!";
 *    Chunk myChunk = new Chunk();
 *
 *    // .append() and .set() may be called in any order
 *    // because tag replacement is delayed until the .toString() call.
 *    myChunk.append( templateBody );
 *    myChunk.set("name", user.getName());
 *    myChunk.set("balance", user.getBalance());
 *    System.out.println( myChunk.toString() );
 *
 *    // reset values and re-use -- original templates are not modified
 *    // when .toString() output is generated.
 *    myChunk.set("name", user2.getName());
 *    myChunk.set("balance", user2.getBalance());
 *    System.out.println( myChunk.toString() );
 *
 * 
* *

* The .toString() method transparently invokes the find and replace
* functionality. * *

* FREQUENTLY ASKED QUESTIONS * *

* Q: If I name things just right, will subtemplates get automatically
* connected to like-named tags?
ie, if I write a template file like this: * *

 * bla bla bla {$myTemplate} foo foo foo
 * {#myTemplate}Hello {$name}!{#}
 * 
* *

* A: No*. To keep things simple and reduce potential for confusion, Chunk
* does not auto-magically fill any tags based on naming conventions or
* in-template directives. You must explicitly invoke the include command:
* * bla bla bla {% include #myTemplate %} foo foo foo * *

* Actually, this documentation is outdated, and several extensions to the
* original template syntax are now available: * *

There is now a powerful new tag modifier syntax which supports:
* * providing default values for tags in-template
* * automating template placement with "includes"
* * in-template text filters including perl-style regex and sprintf
* * macro-style templating
* * extending the system to access alternate template repositories
* *

Complete details are here: http://www.x5software.com/chunk * *

* Q: My final output says "infinite recursion detected." What gives? * *

* A: You tripped the recursion depth limit (17) or you did some variation of this: *

 *   TEMPLATE:
 *     bla bla bla {$name}
 *     {#name_info}My name is {$name}{#}
 *
 *   CODE:
 *     ...set("name", theme.getSnippet("file#name_info"));
 *     ...toString();
 * 
* *

* The outer template gets its {$name} tag replaced with "My name is {$name}" --
* then, that replacement value is scanned for any tags that might need to be
* swapped out for their values. It finds {$name} and, using the rule you gave,
* replaces it with "My name is {$name}" so we now have My name is My name is ...
* ad infinitum. * *

* This situation is detected by assuming recursion depth will not normally go
* deeper than 17. If you legitimately need to nest templates that deep, you
* can flatten out the recursion by doing a .toString() expansion partway
* through the nest OR you can tweak the depth limit value in the Chunk.java
* source code. * *

* Q: Where did my subtemplates go? * *

* A: TemplateSet parses out subtemplates and leaves no trace of them in the
* outer template where they were defined. Some people are surprised when
* no placeholder tag is automagically generated and left in place of the
* subtemplate definition -- sorry, this is not the convention. For optional
* elements the template/code usually looks like this: * *

 *   TEMPLATE:
 *     bla bla bla
 *     {$memberMenu:}
 *     {#member_menu}this that etc{#}
 *     foo foo foo
 *
 *   CODE:
 *     if (isLoggedIn) {
 *          myChunk.set("memberMenu", theme.getSnippet("file#member_menu"));
 *     }
 * 
* *

* The subtemplate does not need to be defined right next to the tag where
* it will be used (although that practice does promote readability). * *

* Q: Are tag names and subtemplate names case sensitive? * *

* A: Yes. I prefer to use snake case in {$tag_names} but be aware
* that {$tag} and {$Tag} and {$TAG} are three different values.
* Similarly, I prefer lowercase with underscores for all
* {#sub_template_names}{#} since templates tend to be defined within html
* files which are typically named in all lowercase. * *

* ADVANCED USES * *

* Chunk works great for the simple examples above, but Chunk can do
* so much more... the find-and-replace alg is recursive, which is
* extremely handy once you get the hang of it. * *

* Internally it resists creating an expensive Hashtable until certain
* threshhold conditions are reached. * *

* Output can be constructed on-the-fly with .append() -- say
* you're building an HTML table but don't know ahead of time how
* many rows and columns it will contain, just loop through all
* your data with calls to .append() after preparing each cell and row: * *

 *    Chunk table = templates.makeChunk("my_table");
 *    Chunk rows = templates.makeChunk();
 *    Chunk row = templates.makeChunk("my_table.my_row");
 *    Chunk cell = templates.makeChunk("my_table.my_cell");
 *
 *    rows.set("background_color", getRowColor() );
 *
 *    while (dataSet.hasMoreData()) {
 *        DataObj data = dataSet.nextDataObj();
 *        String[] attributes = data.getAttributes();
 *
 *        StringBuilder cells = new StringBuilder();
 *        for (int i=0; i < attributes.length; i++) {
 *            cell.set("cellContent", attributes[i]);
 *            cells.append( cell.toString() );
 *        }
 *
 *        row.set("name", data.getName());
 *        row.set("id", data.getID());
 *        row.set("cells",cells);
 *
 *        rows.append( row.toString() );
 *    }
 *
 *    table.set("table_rows",rows);
 *    System.out.println( table.toString() );
 * 
* *
 * Possible contents of my_table.html:
 *
 * <TABLE>
 * {$table_rows}
 * </TABLE>
 *
 * {#my_row}
 * <TR bgcolor="{$background_color}">
 *  <TD>{$id} - {$name}</TD>
 *  {$cells}
 * </TR>
 * {#}
 *
 * {#my_cell}
 * <TD>{$cell_content}</TD>
 * {#}
 * 
* * Copyright: waived, free to use
* Company: X5 Software
* Updates: Chunk Documentation
* * @author Tom McClure * @version 3.2.4 */ public class Chunk implements Map { public static final int HASH_THRESH = 8; public static final int DEPTH_LIMIT = 17; public static final String VERSION = "3.2.4"; public static final String TRUE = "TRUE"; protected Snippet templateRoot = null; private String templateOrigin = null; private String[] firstTags = new String[HASH_THRESH]; private Object[] firstValues = new Object[HASH_THRESH]; private int tagCount = 0; protected Vector template = null; private Hashtable tags = null; protected String tagStart = TemplateSet.DEFAULT_TAG_START; protected String tagEnd = TemplateSet.DEFAULT_TAG_END; private Vector> contextStack = null; private ContentSource macroLibrary = null; private ChunkFactory chunkFactory = null; private String localeCode = null; private ChunkLocale locale = null; // print errors to output? private boolean renderErrs = true; private PrintStream errLog = null; // package visibility void setMacroLibrary(ContentSource repository, ChunkFactory factory) { this.macroLibrary = repository; if (altSources != null) { addProtocol(repository); } this.chunkFactory = factory; } public ContentSource getTemplateSet() { return this.macroLibrary; } public void setChunkFactory(ChunkFactory factory) { this.chunkFactory = factory; } public ChunkFactory getChunkFactory() { return chunkFactory; } public void append(Snippet toAdd) { // don't bother with overhead of vector until necessary if (templateRoot == null && template == null) { templateRoot = toAdd; } else { if (template == null) { template = new Vector(); template.addElement(templateRoot); template.addElement(toAdd); } else { template.addElement(toAdd); } } } /** * Add a String on to the end a Chunk's template. */ public void append(String toAdd) { if (toAdd == null) return; // parse string into snippets-to-process and literals etc. Snippet snippet = Snippet.getSnippet(toAdd); append(snippet); } /** * Add a Chunk on to the end of a Chunk's "template" -- this "child" * Chunk won't get it's .toString() invoked until the parent Chunk's * tags are replaced, ie when the parent Chunk's .toString() method * is invoked. The child does NOT get cloned in this call so if * you append a chunk, alter its tags and append it again, you'll * see it in its final state twice after the template expansion. * To keep incremental changes, use append(child.toString()) instead. */ public void append(Chunk toAdd) { // if we're adding a chunk we'll almost definitely add more than one. // switch to vector if (template == null) { template = new Vector(); if (templateRoot != null) template.addElement(templateRoot); } // internally, we stash in tag table and wrap in Snippet. String chunkKey = ";CHUNK_" + toAdd.hashCode(); set(chunkKey,toAdd); String autoTag = makeTag(chunkKey); template.addElement(Snippet.getSnippet(autoTag)); } /** * Creates a find-and-replace rule for tag replacement. Overwrites any * previous rules for this tagName. Do not include the tag boundary * markers in the tagName, ie * GOOD: set("this", "that") * BAD: set("{$this}", "that") * * @param tagName will be ignored if null. * @param tagValue will be translated to the empty String if null -- use setOrDelete() instead of set() if you don't need/want this behavior. */ public void set(String tagName, String tagValue) { set(tagName, tagValue, ""); } /** * Creates a find-and-replace rule for tag replacement. See remarks * at append(Chunk c). * @param tagName will be ignored if null. * @param tagValue will be translated to the empty String if null. * @see #append(Chunk) */ public void set(String tagName, Chunk tagValue) { set(tagName, tagValue, ""); } /** * Convenience method, chains to set(String s, Object o, String ifNull) */ public void set(String tagName, Object tagValue) { String ifNull = null; set(tagName, tagValue, ifNull); } /** * Careful, setOrDelete will DELETE a previous value * for the tag at this level if passed a null value. * * This is a way around the standard behavior which interprets * null values as the empty string. * * The default is to use the empty string for null object/strings. * setOrDelete provides an alternate option, leaving the tag "unset" * in case the value is null, which allows the template to provide * its own default value. */ public void setOrDelete(String tagName, Object tagValue) { if (tagValue == null) { // containsKey handy side effect -- converts to hashtable if nec. if (this.containsKey(tagName)) { // unset!! tags.remove(tagName); } return; } else { set(tagName, tagValue, null); } } /** * setLiteral() tag values will render verbatim, so even if the value * contains tags/specials they will not be expanded. However, if * the final engine output is passed back into the chunk processor * as a static string (for example, using chunk to pre-generate table * rows that are destined for placement in another chunk), it will not * be protected from the engine in that second pass. * * To prevent re-processing higher up the chain, encase your string * in {% literal %}...{% endliteral %} tags, with the tradeoff that * they will appear in your final output. This is by design, so the * literal will be preserved even after multiple passes through the * engine. * * Typical workaround: ... * * Or, just be super-careful to use setLiteral() again when placing * pre-processed output into higher-level chunks. * * @param tagName * @param literalValue */ public void setLiteral(String tagName, String literalValue) { Snippet hardValue = Snippet.makeLiteralSnippet(literalValue); set(tagName, hardValue); } /** * Create a tag replacement rule, supplying a default value in case * the value passed is null. If both the tagValue and the fallback * are null, the rule created will resolve all instances of the tag * to the string "NULL" * @param tagName tag to replace * @param tagValue replacement value -- no-op unless this is of type String or Chunk. * @param ifNull fallback replacement value in case tagValue is null */ public void set(String tagName, Object tagValue, String ifNull) { // all "set" methods eventually chain to here if (tagName == null) return; // ensure that tagValue is either a String or a Chunk (or some tabular data) if (tagValue != null) { tagValue = coercePrimitivesToStringAndBoxAliens(tagValue); } if (tagValue == null) { tagValue = (ifNull == null) ? "NULL" : ifNull; } if (tags != null) { tags.put(tagName,tagValue); } else { // sequential scan is kinda inefficient but // completely acceptable for small datasets for (int i=0; i= HASH_THRESH) { // threshhold reached, upgrade to hashtable tags = new Hashtable(HASH_THRESH * 2); copyToHashtable(); tags.put(tagName,tagValue); } else { firstTags[tagCount] = tagName; firstValues[tagCount] = tagValue; tagCount++; } } } /** * Make bean properties available to template */ public void setToBean(String tagName, Object bean) { setToBean(tagName, bean, null); } /** * Make bean properties available to template */ @SuppressWarnings("rawtypes") public void setToBean(String tagName, Object bean, String ifNull) { Map boxedBean = ObjectDataMap.wrapBean(bean); set(tagName, boxedBean, ifNull); } /** * For convenience, sets a flag to "TRUE" - to reverse, call unset("flag") */ public void set(String tagName) { set(tagName, TRUE); } /** * For convenience, auto-converts int to String and creates * tag replacement rule. Overwrites any existing rule with this tagName. */ public void set(String tagName, int tagValue) { set(tagName, Integer.toString(tagValue)); } /** * For convenience, auto-converts char to String and creates * tag replacement rule. Overwrites any existing rule with this tagName. */ public void set(String tagName, char tagValue) { set(tagName, Character.toString(tagValue)); } /** * For convenience, auto-converts long to String and creates * tag replacement rule. Overwrites any existing rule with this tagName. */ public void set(String tagName, long tagValue) { set(tagName, Long.toString(tagValue)); } /** * For convenience, auto-converts StringBuilder to String and creates * tag replacement rule. Overwrites any existing rule with this tagName. */ public void set(String tagName, StringBuilder tagValue) { if (tagValue != null) set(tagName, tagValue.toString()); } /** * For convenience, calls .set(tagName, "TRUE") for true values * and .unset(tagName) for false values -- ie since the string * "FALSE" would evaluate to true in a template if-expression. */ public void set(String tagName, boolean value) { if (value) { set(tagName, TRUE); } else { unset(tagName); } } /** * For convenience, auto-converts StringBuffer to String and creates * tag replacement rule. Overwrites any existing rule with this tagName. */ public void set(String tagName, StringBuffer tagValue) { if (tagValue != null) set(tagName, tagValue.toString()); } /** * unset("tag") deletes the named tag expansion rule from the ruleset. * @param tagName */ public void unset(String tagName) { if (tagName != null) { setOrDelete(tagName, null); } } /** * @return true if a rule exists for this tagName, otherwise false. * Returns false if tagName is null. */ public boolean hasValue(String tagName) { if (tagName == null) return false; if (tags != null) { return tags.containsKey(tagName); } else { for (int i=0; i parentContext = context.prepareParentContext(); explodeForParentToPrinter(out, parentContext); } private void pushContextStack(Vector parentContext) { if (contextStack == null) { contextStack = new Vector>(); } contextStack.insertElementAt(parentContext, 0); } private void popContextStack() { if (contextStack == null || contextStack.size() == 0) return; contextStack.removeElementAt(0); } private void explodeForParentToPrinter(Writer out, Vector ancestors) throws IOException { if (template == null && templateRoot == null) return; if (ancestors != null) { // PUSH ANCESTORS ONTO STACK AND LOCK DOWN synchronized(this) { pushContextStack(ancestors); renderForParentToPrinter(out); popContextStack(); } } else { renderForParentToPrinter(out); } } private void renderForParentToPrinter(Writer out) throws IOException { if (template == null) { explodeToPrinter(out, templateRoot, 1); } else { // If template was constructed incrementally, with several .append(...) calls, // some block-open tags might not be grouped with the matching block-closed tag. // Merge templates together into a single Snippet if possible. if (template.size() > 1) { template = mergeTemplateParts(); } for (int i=0; i < template.size(); i++) { Snippet s = template.elementAt(i); explodeToPrinter(out, s, 1); } } } private Vector mergeTemplateParts() { Snippet merged; try { merged = Snippet.consolidateSnippets(template); } catch (EndOfSnippetException e) { return template; } Vector newTemplate = new Vector(); newTemplate.add(merged); return newTemplate; } void explodeToPrinter(Writer out, Object obj, int depth) throws IOException { if (depth >= DEPTH_LIMIT) { String err = handleError("[**ERR** max template recursions: "+DEPTH_LIMIT+"]"); if (err != null) out.append(err); } else if (obj instanceof Snippet) { Snippet snippet = (Snippet)obj; snippet.render(out, this, depth); } else if (obj instanceof String) { // snippet-ify to catch/skip literal blocks Snippet snippet = Snippet.getSnippet((String)obj); explodeToPrinter(out, snippet, depth); } else if (obj instanceof Chunk) { Vector parentContext = prepareParentContext(); Chunk c = (Chunk) obj; c.explodeForParentToPrinter(out, parentContext); } else if (obj instanceof DataCapsule[]) { // auto-expand? DataCapsuleReader reader = DataCapsuleReader.getReader((DataCapsule[])obj); String err = handleError("[LIST("+reader.getDataClassName()+") - Use a loop construct to display list data.]"); if (err != null) out.append(err); } else if (obj instanceof String[]) { String err = handleError("[LIST(java.lang.String) - Use a loop construct to display list data, or pipe to join().]"); if (err != null) out.append(err); } else if (obj instanceof List) { String err = handleError("[LIST - Use a loop construct to display list data, or pipe to join().]"); if (err != null) out.append(err); } else { // implicit cast to string String repr = ObjectDataMap.getAsString(obj); explodeToPrinter(out, repr, depth); } } @SuppressWarnings("unchecked") private Vector prepareParentContext() { if (contextStack == null) { Vector parentContext = new Vector(); parentContext.add(this); return parentContext; } else { // current context is first element on stack Vector parentContext = contextStack.firstElement(); parentContext = (Vector)parentContext.clone(); parentContext.insertElementAt(this,0); return parentContext; } } private Vector getCurrentParentContext() { if (contextStack == null || contextStack.size() == 0) { return null; } else { // current context is first element on stack return contextStack.firstElement(); } } /** * Retrieves a tag replacement rule. getTagValue() responds outside the context * of recursive tag replacement, so the return value may include unresolved * tags. To iterate up the ancestor chain, use get() instead. * * @return The Chunk or Snippet etc. that this tag will resolve to, or null * if no rule yet exists. */ public Object getTagValue(String tagName) { if (tags != null) { Object x = tags.get(tagName); if (x instanceof String) { // first request for this value. lazy-convert to Snippet. // subsequent fetches will benefit from pre-scan. Snippet s = Snippet.getSnippet((String)x); tags.put(tagName, s); return s.isSimple() ? s.toString() : s; } else if (x instanceof Snippet) { Snippet s = (Snippet)x; return s.isSimple() ? s.toString() : s; } else { return x; } } else { for (int i=0; i altSources = null; public void addProtocol(ContentSource src) { if (altSources == null) { altSources = new Hashtable(); // delayed adding macro library for memory efficiency // (avoid overhead of hashtable whenever possible) if (macroLibrary != null) { altSources.put(macroLibrary.getProtocol(), macroLibrary); } } String protocol = src.getProtocol(); altSources.put(protocol,src); } private Object altFetch(String tagName, int depth) { return altFetch(tagName, depth, false); } private static final java.util.regex.Pattern INCLUDEIF_PATTERN = java.util.regex.Pattern.compile("^\\.include(If|\\.\\()"); private Object altFetch(String tagName, int depth, boolean ignoreParentContext) { String tagValue = null; // the .calc(...) fn if (tagName.startsWith(".calc(")) { String eval = null; try { eval = Calc.evalCalc(tagName,this); } catch (NoClassDefFoundError e) { String errMsg = "[ERROR: jeplite jar missing from classpath! .calc command requires jeplite library]"; eval = handleError(errMsg); } return eval; } if (tagName.startsWith(".version")) { return VERSION; } // the .loop(...) fn if (tagName.startsWith(".loop")) { return LoopTag.expandLoop(tagName, this, this.templateOrigin, depth); } // the .tagStack fn if (tagName.startsWith(".tagStack")) { String format = "text"; if (tagName.contains("html")) { format = "html"; } return this.formatTagStack(format); } if (altSources == null && macroLibrary == null && getCurrentParentContext() == null) { // it ain't there to fetch return null; } // the includeIfPattern (defined above) // matches ".includeIf" and ".include.(" <-- ie from +(cond) expansion Matcher m = INCLUDEIF_PATTERN.matcher(tagName); if (m.find()) { // this is either lame or very sneaky String translation = Filter.translateIncludeIf(tagName,tagStart,tagEnd,this); return translation; } // parse content source "protocol" int delimPos = tagName.indexOf(".",1); int spacePos = tagName.indexOf(" ",1); // {.include abc#xyz} is ok too if (delimPos < 0 && spacePos < 0) { if (tagName.startsWith("./")) { // extra end tag, pass through return null; //return "[CHUNK_ERR: extra end tag, no matching tag found for "+tagName.substring(1)+"]"; } else { String errMsg = "[CHUNK_ERR: malformed content reference: '"+tagName+"' -- missing argument]"; return handleError(errMsg); } } if (spacePos > 0 && (delimPos < 0 || spacePos < delimPos)) delimPos = spacePos; String srcName = tagName.substring(1,delimPos); String itemName = tagName.substring(delimPos+1); // for this to work, caller must have already provided an object which // implements com.x5.template.ContentSource // -- then templates can delegate to this source using the syntax // {.protocol.itemName} eg {.wiki.About_Us} or {.include.#some_template} // strip away filters, defaults String cleanItemName = itemName; cleanItemName = cleanItemName.replaceAll("[\\|:].*$", ""); ContentSource fetcher = null; if (altSources != null) { fetcher = altSources.get(srcName); } else if (macroLibrary != null && srcName.equals(macroLibrary.getProtocol())) { // if the only alt source is the macro library for includes, // no hashtable is made (for memory efficiency) fetcher = macroLibrary; } // when altSources exists, it handles includes too if (fetcher != null) { if (fetcher instanceof Theme) { // include's are special, handle via macroLibrary TemplateSet // slight optimization, return Snippet instead of String Theme theme = (Theme)fetcher; String templateRef = BlockTag.qualifyTemplateRef(templateOrigin, cleanItemName); Snippet s = theme.getSnippet(templateRef); if (s != null) return s; } else { tagValue = fetcher.fetch(cleanItemName); } } if (tagValue == null && !ignoreParentContext) { // still null? maybe an ancestor knows how to grok Vector parentContext = getCurrentParentContext(); if (parentContext != null) { for (Chunk ancestor : parentContext) { // lazy... should repeat if/else above to avoid re-parsing the tag Object x = ancestor.altFetch(tagName, depth, true); if (x != null) return x; } } } return tagValue; } private String resolveBackticks(String lookupName, int depth) { int backtickA = lookupName.indexOf('`'); if (backtickA < 0) return lookupName; int backtickB = lookupName.indexOf('`',backtickA+1); if (backtickB < 0) return lookupName; String embeddedTag = lookupName.substring(backtickA+2,backtickB); char typeChar = lookupName.charAt(backtickA+1); if (typeChar == '^' || typeChar == '.') { embeddedTag = '.'+embeddedTag; } else if (typeChar != '~' && typeChar != '$') { // only ^ and ~ (and $) are legal for now return lookupName; } Object backtickExprValue = resolveTagValue(embeddedTag, depth); if (backtickExprValue == null) { return lookupName; } else { String dynLookupName = lookupName.substring(0,backtickA) + backtickExprValue + lookupName.substring(backtickB+1); // there may be more... return resolveBackticks(dynLookupName, depth); } } protected Object resolveTagValue(SnippetTag tag, int depth, String origin) { if (origin == null) { return _resolveTagValue(tag, depth, false); } else { this.templateOrigin = origin; Object value = _resolveTagValue(tag, depth, false); this.templateOrigin = null; return value; } } protected Object resolveTagValue(SnippetTag tag, int depth) { return _resolveTagValue(tag, depth, false); } @SuppressWarnings("rawtypes") protected Object _resolveTagValue(SnippetTag tag, int depth, boolean ignoreParentContext) { String[] path = tag.getPath(); int segment = 0; String segmentName = path[segment]; if (tag.hasBackticks()) { segmentName = resolveBackticks(segmentName, depth); } Object tagValue = null; if (segmentName.charAt(0) == '.') { tagValue = altFetch(segmentName, depth); } else if (hasValue(segmentName)) { tagValue = getTagValue(segmentName); } else { if (ignoreParentContext) { return tagValue; } Vector parentContext = getCurrentParentContext(); if (parentContext != null) { // now look in ancestors (iteration, not recursion, so sue me) for (Chunk ancestor : parentContext) { tagValue = ancestor.getTagValue(segmentName); ////tagValue = ancestor._resolveTagValue(tag, depth, true); if (tagValue != null) break; } } } segment++; // If path has more segments, drill deeper until reference is resolved // or path-map hits dead end while (path.length > segment && tagValue != null) { if (tagValue instanceof Map) { segmentName = path[segment]; if (tag.hasBackticks()) { segmentName = resolveBackticks(segmentName, depth); } Map obj = (Map)tagValue; tagValue = obj.get(segmentName); segment++; // Sometimes dotted tags are not actually buried // deep inside objects... cf LoopTag $x.first // This is a hacky way to allow single-depth refs to work if (tagValue == null && path.length == segment) { String fakeRef = path[segment-2] + "." + segmentName; tagValue = getTagValue(fakeRef); } } else { tagValue = null; } } // convert primitives to string, box illegal aliens if (tagValue != null && !(tagValue instanceof String)) { tagValue = coercePrimitivesToStringAndBoxAliens(tagValue); } Filter[] filters = tag.getFilters(); if (tagValue == null) { String tagDefault = tag.getDefaultValue(); if (filters != null && (tag.applyFiltersFirst() || tagDefault == null)) { // filtering may result in null being transformed to not null Object filteredNull = Filter.applyFilter(this, filters, null); if (filteredNull != null) { return filteredNull; } } if (tag.applyFiltersFirst()) { return tagDefault; } else if (filters != null) { return Filter.applyFilter(this, filters, tagDefault); } else { return tagDefault; } } else { if (filters == null) { return tagValue; } else { Object filteredVal = Filter.applyFilter(this, filters, tagValue); if (filteredVal == null && tag.applyFiltersFirst()) { return tag.getDefaultValue(); } else { return filteredVal; } } } } // unbox and stringify primitive wrapper objects, box any objects if not chunk-friendly private static Object coercePrimitivesToStringAndBoxAliens(Object o) { if (o == null) return o; if (o instanceof Boolean) { return ((Boolean)o).booleanValue() ? TRUE : null; } else if (ObjectDataMap.isWrapperType(o.getClass())) { return o.toString(); } else { return boxIfAlienObject(o); } } public static Object boxIfAlienObject(Object o) { if (o == null) return o; if (o instanceof Chunk || o instanceof TableData) { // Chunk and TableData can be handled natively return o; } else if (o instanceof Map) { // Map can be handled natively return o; } else if (o instanceof String || o instanceof Snippet || o instanceof List || o instanceof Object[]) { // can all be handled natively return o; } // unrecognized object. wrap inside map. return new ObjectDataMap(o); } protected Object resolveTagValue(String tagName, int depth) { return _resolveTagValue(SnippetTag.parseTag(tagName), depth, false); } /** * Clears all tag replacement rules. */ public void resetTags() { if (tags != null) { tags.clear(); } else { tagCount = 0; } } public void clear() { resetTags(); } /** * Clears template */ public void resetTemplate() { if (this.template == null) { this.templateRoot = null; } else { this.template.clear(); } } public boolean containsKey(Object key) { if (tags == null) { tags = new Hashtable(); copyToHashtable(); } return tags.containsKey(key); } public boolean containsValue(Object value) { if (tags == null) { tags = new Hashtable(); copyToHashtable(); } return tags.containsValue(value); } public Set> entrySet() { if (tags == null) { tags = new Hashtable(); copyToHashtable(); } return tags.entrySet(); } public boolean equals(Object o) { if (tags == null) { tags = new Hashtable(); copyToHashtable(); } return tags.equals(o); } public Object get(Object key) { return resolveTagValue((String)key, 1); } public int hashCode() { if (tags == null) { tags = new Hashtable(); copyToHashtable(); } return tags.hashCode(); } public boolean isEmpty() { if (tags == null) { return tagCount == 0; } else { return tags.isEmpty(); } } public java.util.Set keySet() { if (tags == null) { tags = new Hashtable(); copyToHashtable(); } return tags.keySet(); } public Object put(String key, Object value) { Object x = getTagValue(key); set(key, value, ""); return x; } public Object remove(Object key) { throw new UnsupportedOperationException(); } @SuppressWarnings({ "unchecked", "rawtypes" }) public void putAll(Map t) { if (t == null || t.size() < 0) return; java.util.Set set = t.keySet(); java.util.Iterator i = set.iterator(); while (i.hasNext()) { String tagName = i.next(); set(tagName, t.get(tagName), ""); } } public int size() { if (tags != null) return tags.size(); return tagCount; } public java.util.Collection values() { if (tags == null) { tags = new Hashtable(); copyToHashtable(); } return tags.values(); } /** * Adds multiple find-and-replace rules using all entries in the * Hashtable. Replaces an existing rule if tagNames collide. */ public void setMultiple(Map rules) { if (rules == null || rules.size() <= 0) return; Set keys = rules.keySet(); for (String tagName : keys) { setOrDelete(tagName, rules.get(tagName)); } } /** * Adds multiple find-and-replace rules using all rules from the passed * Chunk. Replaces any existing rules with the same tagName. */ public void setMultiple(Chunk copyFrom) { if (copyFrom != null) { Map h = copyFrom.getTagsTable(); setMultiple(h); } } /** * Retrieve all find-and-replace rules. Alterations to the returned * Hashtable WILL AFFECT the tag replacement rules of the Chunk directly. * Does not return a clone. * @return a Hashtable containing the Chunk's find-and-replace rules. */ public Map getTagsTable() { if (tags != null) { return tags; } else { if (tagCount <= 0) { return null; } else { copyToHashtable(); return tags; } } } private void copyToHashtable() { if (tags == null) tags = new Hashtable(tagCount*2); for (int i=0; i parentContext = getCurrentParentContext(); if (parentContext != null) { for (Chunk ancestor : parentContext) { ancestor.outputTags(stack,lineFeed,indent,indentLevel); indentLevel++; } } return stack.toString(); } private void outputTags(StringBuilder output, String lf, String ind, int indent) { ArrayList list = new ArrayList(); if (tags == null) { for (int i=0; i -1) { sb.append(toSearch.substring(marker,findPos)); sb.append(replace); marker = findPos+findLen; } sb.append(toSearch.substring(marker)); return sb.toString(); } public String getTemplateOrigin() { return this.templateOrigin; } }