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: 0.2.0
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.HashMap;
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 { /** * 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); } protected Template (Segment[] segs, Mustache.Compiler compiler) { _segs = segs; _compiler = compiler; } protected void executeSegs (Context ctx, Writer out) throws MustacheException { for (Segment seg : _segs) { seg.execute(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) { if (!_compiler.standardsMode) { // if we're dealing with a compound key, resolve each component and use the result to // resolve the subsequent component and so forth if (name != DOT_NAME && name.indexOf(DOT_NAME) != -1) { 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( "Missing context for compound variable '" + name + "' on line " + line + ". '" + comps[ii - 1] + "' was not found."); 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); } } // 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, we don't search our parent contexts if (_compiler.standardsMode) { return checkForMissing(name, line, missingIsNull, getValueIn(ctx.data, name, line)); } boolean variableMissing = true; while (ctx != null) { Object value = getValueIn(ctx.data, name, line); if (value == NO_FETCHER_FOUND) { // preserve variableMissing } else if (value == null) { // we found a fetcher, and it returned null; so we keep searching our parents, but // we won't freak out about a missing variable if we have a nullValue configured variableMissing = false; } else { return value; } ctx = ctx.parent; } // we've popped off the top of our stack of contexts, if we never actually found a fetcher // for our variable, we need to let checkForMissing() know return checkForMissing(name, line, missingIsNull, variableMissing ? NO_FETCHER_FOUND : null); } /** * Returns the value of the specified variable, noting that it is intended to be used as the * contents for a segment. Presently this does not do anything special, but eventually this * will be the means by which we enact configured behavior for sections that reference null or * missing variables. Right now, all such variables result in a length 0 section. */ protected Object getSectionValue (Context ctx, String name, int line) { // TODO: configurable behavior on missing values Object value = getValue(ctx, name, line, _compiler.missingIsNull); // 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.nullValue : 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; synchronized (_fcache) { 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, just return null and our caller can either try // the parent context, or do le freak out if (fetcher == null) { return NO_FETCHER_FOUND; } try { Object value = fetcher.get(data, name); synchronized (_fcache) { _fcache.put(key, fetcher); } return value; } catch (Exception e) { throw new MustacheException( "Failure fetching variable '" + name + "' on line " + 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( "No method or field with name '" + name + "' on line " + line); } else { return value; } } protected final Segment[] _segs; protected final Mustache.Compiler _compiler; protected final Map _fcache = new HashMap(); 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, 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); 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; } } protected static final Object NO_FETCHER_FOUND = new Object(); 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(); }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy