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

com.samskivert.mustache.Template Maven / Gradle / Ivy

There is a newer version: 1.16
Show newest version
//
// JMustache - A Java implementation of the Mustache templating language
// http://github.com/samskivert/jmustache/blob/master/LICENSE

package com.samskivert.mustache;

import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;

/**
 * Represents a compiled template. Templates are executed with a context to generate
 * output. The context can be any tree of objects. Variables are resolved against the context.
 * Given a name {@code foo}, the following mechanisms are supported for resolving its value
 * (and are sought in this order):
 * 
    *
  • If the variable has the special name {@code this} the context object itself will be * returned. This is useful when iterating over lists. *
  • If the object is a {@link Map}, {@link Map#get} will be called with the string {@code foo} * as the key. *
  • A method named {@code foo} in the supplied object (with non-void return value). *
  • A method named {@code getFoo} in the supplied object (with non-void return value). *
  • A field named {@code foo} in the supplied object. *
*

The field type, method return type, or map value type should correspond to the desired * behavior if the resolved name corresponds to a section. {@link Boolean} is used for showing or * hiding sections without binding a sub-context. Arrays, {@link Iterator} and {@link Iterable} * implementations are used for sections that repeat, with the context bound to the elements of the * array, iterator or iterable. Lambdas are current unsupported, though they would be easy enough * to add if desire exists. See the Mustache * documentation for more details on section behavior.

*/ public class Template { /** * Encapsulates a fragment of a template that is passed to a lambda. The fragment is bound to * the variable context that was in effect at the time the lambda was called. */ public abstract class Fragment { /** Executes this fragment; writes its result to {@code out}. */ public abstract void execute (Writer out); /** Executes this fragment with the provided context; writes its result to {@code out}. The * provided context will be nested in the fragment's bound context. */ public abstract void execute (Object context, Writer out); /** Executes {@code tmpl} using this fragment's bound context. This allows a lambda to * resolve its fragment to a dynamically loaded template and then run that template with * the same context as the lamda, allowing a lambda to act as a 'late bound' included * template, i.e. you can decide which template to include based on information in the * context. */ public abstract void executeTemplate (Template tmpl, Writer out); /** Executes this fragment and returns its result as a string. */ public String execute () { StringWriter out = new StringWriter(); execute(out); return out.toString(); } /** Executes this fragment with the provided context; returns its result as a string. The * provided context will be nested in the fragment's bound context. */ public String execute (Object context) { StringWriter out = new StringWriter(); execute(context, out); return out.toString(); } /** Returns the context object in effect for this fragment. The actual type of the object * depends on the structure of the data passed to the top-level template. You know where * your lambdas are executed, so you know what type to which to cast the context in order * to inspect it (be that a {@code Map} or a POJO or something else). */ public abstract Object context (); /** Like {@link #context()} btu returns the {@code n}th parent context object. {@code 0} * returns the same value as {@link #context()}, {@code 1} returns the parent context, * {@code 2} returns the grandparent and so forth. Note that if you request a parent that * does not exist an exception will be thrown. You should only use this method when you * know your lambda is run consistently in a context with a particular lineage. */ public abstract Object context (int n); /** Decompiles the template inside this lamdba and returns an approximation of * the original template from which it was parsed. This is not the exact character for * character representation because the original text is not preserved because that would * incur a huge memory penalty for all users of the library when the vast majority of * them do not call decompile. * *

Limitations: *

  • Whitespace inside tags is not preserved: i.e. {@code {{ foo.bar }}} becomes * {@code {{foo.bar}}}. *
  • If the delimiters are changed by the template, those are not preserved. * The delimiters configured on the {@link Compiler} are used for all decompilation. *
* *

This feature is meant to enable use of lambdas for i18n such that you can recover * the contents of a lambda (so long as they're simple) to use as the lookup key for a * translation string. For example: {@code {{#i18n}}Hello {{user.name}}!{{/i18n}}} can be * sent to an {@code i18n} lambda which can use {@code decompile} to recover the text * {@code Hello {{user.name}}!} to be looked up in a translation dictionary. The * translated fragment could then be compiled and cached and then executed in lieu of the * original fragment using {@link Template.Fragment#context}. */ public String decompile () { return decompile(new StringBuilder()).toString(); } /** Decompiles this fragment into {@code into}. See {@link #decompile()}. * @return {@code into} for call chaining. */ public abstract StringBuilder decompile (StringBuilder into); } /** A sentinel object that can be returned by a {@link Mustache.Collector} to indicate that a * variable does not exist in a particular context. */ public static final Object NO_FETCHER_FOUND = new String(""); /** * Executes this template with the given context, returning the results as a string. * @throws MustacheException if an error occurs while executing or writing the template. */ public String execute (Object context) throws MustacheException { StringWriter out = new StringWriter(); execute(context, out); return out.toString(); } /** * Executes this template with the given context, writing the results to the supplied writer. * @throws MustacheException if an error occurs while executing or writing the template. */ public void execute (Object context, Writer out) throws MustacheException { executeSegs(new Context(context, null, 0, false, false), out); } /** * Executes this template with the supplied context and parent context, writing the results to * the supplied writer. The parent context will be searched for variables that cannot be found * in the main context, in the same way the main context becomes a parent context when entering * a block. * @throws MustacheException if an error occurs while executing or writing the template. */ public void execute (Object context, Object parentContext, Writer out) throws MustacheException { Context pctx = new Context(parentContext, null, 0, false, false); executeSegs(new Context(context, pctx, 0, false, false), out); } protected Template (Segment[] segs, Mustache.Compiler compiler) { _segs = segs; _compiler = compiler; _fcache = compiler.collector.createFetcherCache(); } protected void executeSegs (Context ctx, Writer out) throws MustacheException { for (Segment seg : _segs) { seg.execute(this, ctx, out); } } protected Fragment createFragment (final Segment[] segs, final Context currentCtx) { return new Fragment() { @Override public void execute (Writer out) { execute(currentCtx, out); } @Override public void execute (Object context, Writer out) { execute(currentCtx.nest(context), out); } @Override public void executeTemplate (Template tmpl, Writer out) { tmpl.executeSegs(currentCtx, out); } @Override public Object context () { return currentCtx.data; } @Override public Object context (int n) { return context(currentCtx, n); } @Override public StringBuilder decompile (StringBuilder into) { for (Segment seg : segs) seg.decompile(_compiler.delims, into); return into; } private Object context (Context ctx, int n) { return (n == 0) ? ctx.data : context(ctx.parent, n-1); } private void execute (Context ctx, Writer out) { for (Segment seg : segs) { seg.execute(Template.this, ctx, out); } } }; } /** * Called by executing segments to obtain the value of the specified variable in the supplied * context. * * @param ctx the context in which to look up the variable. * @param name the name of the variable to be resolved, which must be an interned string. * @param missingIsNull whether to fail if a variable cannot be resolved, or to return null in * that case. * * @return the value associated with the supplied name or null if no value could be resolved. */ protected Object getValue (Context ctx, String name, int line, boolean missingIsNull) { // handle our special variables if (name == FIRST_NAME) { return ctx.onFirst; } else if (name == LAST_NAME) { return ctx.onLast; } else if (name == INDEX_NAME) { return ctx.index; } // if we're in standards mode, restrict ourselves to simple direct resolution (no compound // keys, no resolving values in parent contexts) if (_compiler.standardsMode) { Object value = getValueIn(ctx.data, name, line); return checkForMissing(name, line, missingIsNull, value); } // first search our parent contexts for the key (even if the key is a compound key, we will // first try to find it "whole" and only if that fails do we resolve it in parts) for (Context pctx = ctx; pctx != null; pctx = pctx.parent) { Object value = getValueIn(pctx.data, name, line); if (value != NO_FETCHER_FOUND) return value; } // if we reach here, we found nothing in this or our parent contexts... // if we have a compound key, decompose the value and resolve it step by step if (name != DOT_NAME && name.indexOf(DOT_NAME) != -1) { return getCompoundValue(ctx, name, line, missingIsNull); } else { // otherwise let checkForMissing() decide what to do return checkForMissing(name, line, missingIsNull, NO_FETCHER_FOUND); } } /** * Decomposes the compound key {@code name} into components and resolves the value they * reference. */ protected Object getCompoundValue (Context ctx, String name, int line, boolean missingIsNull) { String[] comps = name.split("\\."); // we want to allow the first component of a compound key to be located in a parent // context, but once we're selecting sub-components, they must only be resolved in the // object that represents that component Object data = getValue(ctx, comps[0].intern(), line, missingIsNull); for (int ii = 1; ii < comps.length; ii++) { if (data == NO_FETCHER_FOUND) { if (!missingIsNull) throw new MustacheException.Context( "Missing context for compound variable '" + name + "' on line " + line + ". '" + comps[ii - 1] + "' was not found.", name, line); return null; } else if (data == null) { return null; } // once we step into a composite key, we drop the ability to query our parent contexts; // that would be weird and confusing data = getValueIn(data, comps[ii].intern(), line); } return checkForMissing(name, line, missingIsNull, data); } /** * Returns the value of the specified variable, noting that it is intended to be used as the * contents for a section. */ protected Object getSectionValue (Context ctx, String name, int line) { Object value = getValue(ctx, name, line, !_compiler.strictSections); // TODO: configurable behavior on null values? return (value == null) ? Collections.emptyList() : value; } /** * Returns the value for the specified variable, or the configured default value if the * variable resolves to null. See {@link #getValue}. */ protected Object getValueOrDefault (Context ctx, String name, int line) { Object value = getValue(ctx, name, line, _compiler.missingIsNull); // getValue will raise MustacheException if a variable cannot be resolved and missingIsNull // is not configured; so we're safe to assume that any null that makes it up to this point // can be converted to nullValue return (value == null) ? _compiler.computeNullValue(name) : value; } protected Object getValueIn (Object data, String name, int line) { if (data == null) { throw new NullPointerException( "Null context for variable '" + name + "' on line " + line); } Key key = new Key(data.getClass(), name); Mustache.VariableFetcher fetcher = _fcache.get(key); if (fetcher != null) { try { return fetcher.get(data, name); } catch (Exception e) { // zoiks! non-monomorphic call site, update the cache and try again fetcher = _compiler.collector.createFetcher(data, key.name); } } else { fetcher = _compiler.collector.createFetcher(data, key.name); } // if we were unable to create a fetcher, use the NOT_FOUND_FETCHER which will return // NO_FETCHER_FOUND to let the caller know that they can try the parent context or do le // freak out; we still cache this fetcher to avoid repeatedly looking up and failing to // find a fetcher in the same context (which can be expensive) if (fetcher == null) { fetcher = NOT_FOUND_FETCHER; } try { Object value = fetcher.get(data, name); _fcache.put(key, fetcher); return value; } catch (Exception e) { throw new MustacheException.Context( "Failure fetching variable '" + name + "' on line " + line, name, line, e); } } protected Object checkForMissing (String name, int line, boolean missingIsNull, Object value) { if (value == NO_FETCHER_FOUND) { if (missingIsNull) return null; throw new MustacheException.Context( "No method or field with name '" + name + "' on line " + line, name, line); } else { return value; } } protected final Segment[] _segs; protected final Mustache.Compiler _compiler; protected final Map _fcache; protected static class Context { public final Object data; public final Context parent; public final int index; public final boolean onFirst; public final boolean onLast; public Context (Object data, Context parent, int index, boolean onFirst, boolean onLast) { this.data = data; this.parent = parent; this.index = index; this.onFirst = onFirst; this.onLast = onLast; } public Context nest (Object data) { return new Context(data, this, index, onFirst, onLast); } public Context nest (Object data, int index, boolean onFirst, boolean onLast) { return new Context(data, this, index, onFirst, onLast); } } /** A template is broken into segments. */ protected static abstract class Segment { abstract void execute (Template tmpl, Context ctx, Writer out); abstract void decompile (Mustache.Delims delims, StringBuilder into); protected static void write (Writer out, String data) { try { out.write(data); } catch (IOException ioe) { throw new MustacheException(ioe); } } } /** Used to cache variable fetchers for a given context class, name combination. */ protected static class Key { public final Class cclass; public final String name; public Key (Class cclass, String name) { this.cclass = cclass; this.name = name; } @Override public int hashCode () { return cclass.hashCode() * 31 + name.hashCode(); } @Override public boolean equals (Object other) { Key okey = (Key)other; return okey.cclass == cclass && okey.name == name; } @Override public String toString () { return cclass.getName() + ":" + name; } } protected static final String DOT_NAME = ".".intern(); protected static final String THIS_NAME = "this".intern(); protected static final String FIRST_NAME = "-first".intern(); protected static final String LAST_NAME = "-last".intern(); protected static final String INDEX_NAME = "-index".intern(); /** A fetcher cached for lookups that failed to find a fetcher. */ protected static Mustache.VariableFetcher NOT_FOUND_FETCHER = new Mustache.VariableFetcher() { public Object get (Object ctx, String name) throws Exception { return NO_FETCHER_FOUND; } }; }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy