// // 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): *
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 { /** 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 (); /** 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 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(); } /** 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: *
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 Object();
/**
* 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 Object context () {
return currentCtx.data;
}
@Override public void execute (Writer out) {
execute(currentCtx, out);
}
@Override public void execute (Object context, Writer out) {
execute(currentCtx.nest(context, 0, false, false), out);
}
@Override public StringBuilder decompile (StringBuilder into) {
for (Segment seg : segs) seg.decompile(_compiler.delims, into);
return into;
}
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