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

com.sampullara.mustache.Mustache Maven / Gradle / Ivy

The newest version!
package com.sampullara.mustache;

import com.google.common.base.Function;
import com.google.common.collect.MapMaker;
import com.sampullara.util.FutureWriter;
import com.sampullara.util.TemplateFunction;

import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Stack;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.google.common.collect.Iterables.limit;
import static com.google.common.collect.Iterables.transform;
import static com.sampullara.mustache.Scope.EMPTY;
import static com.sampullara.mustache.Scope.NULL;

/**
 * Base class for Mustaches.
 * 

* User: sam * Date: May 3, 2010 * Time: 10:12:47 AM */ public class Mustache { // The implicit element token private static final String IMPLICIT_CURRENT_ELEMENT_TOKEN = "."; // Logging public static Logger logger = Logger.getLogger(Mustache.class.getName()); // Debug configuration variables // Line number tracker for code public static final ThreadLocal line = new ThreadLocal(); // Report missing values public static final boolean debug = Boolean.getBoolean("mustache.debug"); // Trace execution of code public static final boolean trace = Boolean.getBoolean("mustache.trace"); // Profile the time it takes to execute code and construct a hotspots list public static final boolean profile = Boolean.getBoolean("mustache.profile"); // The name of the template private String name; // The code of the template private Code[] compiled; // A reference to the generator of the template protected MustacheJava mj; // The template text protected StringBuilder sb = new StringBuilder(); /** * Given text that was created by or that matches the shape of a mustache * template, return the scope that can be used to recreate the text. * * @param text * @return * @throws MustacheException */ public Scope unexecute(String text) throws MustacheException { AtomicInteger position = new AtomicInteger(0); return unexecute(text, position); } protected Scope unexecute(String text, AtomicInteger position) throws MustacheException { Scope current = new Scope(); for (int i = 0; i < compiled.length; i++) { if (debug) { line.set(compiled[i].getLine()); } Code[] truncate = truncate(compiled, i + 1, new Code[0]); current = compiled[i].unexecute(current, text, position, truncate); if (current == null) { int min = Math.min(position.get() + 50, position.get() + Math.max(0, text.length() - position.get())); int linepos = 0; int lines = 0; while (linepos < min) { int index = text.substring(linepos).indexOf("\n"); if (index == -1) break; linepos = index + linepos + 1; lines++; } throw new MustacheException( "Failed to match template " + compiled[i] + " at " + name + ":" + compiled[i].getLine() + " with text " + text.substring(position.get(), min)+ " at line " + lines); } } return current; } public static Code[] truncate(Code[] codes, int start, Code[] next) { if (codes.length <= 1) return next; Code[] truncate = new Code[codes.length - start]; System.arraycopy(codes, start, truncate, 0, truncate.length); return truncate; } /** * Execute the Mustache using an object as the backing data and write the result * to the provided writer. * * @param writer * @param parent * @return * @throws MustacheException */ public void execute(Writer writer, Object parent) throws MustacheException, IOException { FutureWriter fw = new FutureWriter(writer); execute(fw, new Scope(parent)); fw.flush(); } /** * Execute the Mustache using a scope as the backing data and write the * result to the provided writer. * * @param writer a writer to write to * @param ctx context of the execution * @throws MustacheException */ public void execute(Writer writer, Scope ctx) throws MustacheException, IOException { FutureWriter fw = new FutureWriter(writer); execute(fw, ctx); fw.flush(); } /** * Execute the Mustache using an object as a the backing data. * * @param writer * @param parent * @throws MustacheException */ public void execute(FutureWriter writer, Object parent) throws MustacheException { execute(writer, new Scope(parent)); } /** * Execute the Mustache using the provided Scope as the backing data. * * @param writer * @param ctx * @throws MustacheException */ public void execute(FutureWriter writer, Scope ctx) throws MustacheException { int length = compiled.length; for (int i = 0; i < length; i++) { Code code = compiled[i]; if (Mustache.debug) { Mustache.line.set(code.getLine()); } code.execute(writer, ctx); } } /** * Regenerate the original template. * * @param writer * @throws MustacheException */ public void identity(FutureWriter writer) throws MustacheException { for (Code code : compiled) { if (debug) { line.set(code.getLine()); } code.identity(writer); } } protected ThreadLocal> capturedWriter = new ThreadLocal>() { @Override protected Stack initialValue() { return new Stack(); } }; protected ThreadLocal> actual = new ThreadLocal>() { @Override protected Stack initialValue() { return new Stack(); } }; /** * Used for capturing bodies during evaluation. * * @param writer * @return */ public FutureWriter pushWriter(FutureWriter writer) { Stack capturedStack = capturedWriter.get(); if (capturedStack.size() > 0) { Stack actualStack = actual.get(); actualStack.push(writer); writer = capturedStack.peek(); } return writer; } public void setCompiled(List compiled) { this.compiled = new ArrayList(compiled).toArray(new Code[compiled.size()]); } public Code[] getCompiled() { return compiled; } public String getName() { return name; } public void setName(String filename) { this.name = filename; } /** * Evaluate an if iterable by returning at most 1 iteration. * * @param s * @param name * @return */ public Iterable ifiterable(final Scope s, final String name) { return limit(transform(iterable(s, name), new Function() { public Scope apply(Scope scope) { scope.remove(name); return scope; } }), 1); } /** * Iterate over a named value. If there is only one value return a single value iterator. * * @param s * @param name * @return */ public Iterable iterable(final Scope s, final String name) { MustacheTrace.Event event = null; if (trace) { Object parent = s.getParent(); String traceName = parent == null ? s.getClass().getName() : parent.getClass().getName(); event = MustacheTrace.addEvent("iterable: " + name, traceName); } final Object value = getValue(s, name); if (value instanceof Function) { if (trace) { event.end(); } return function(s, (Function) value); } if (trace) { event.end(); } if (value == null || (value instanceof Boolean && !((Boolean) value))) { return EMPTY; } return new Iterable() { public Iterator iterator() { return new Iterator() { Iterator i; Object iterable = value; public boolean hasNext() { if (i == null) { if (iterable instanceof Future) { try { iterable = ((Future) iterable).get(); if (iterable == null || (iterable instanceof Boolean && !((Boolean) iterable))) { return false; } } catch (Exception e) { throw new RuntimeException(e); } } i = s.getObjectHandler().iterate(iterable); } return i.hasNext(); } public Scope next() { MustacheTrace.Event event = null; if (trace) { Object parent = s.getParent(); String traceName = parent == null ? s.getClass().getName() : parent.getClass().getName(); event = MustacheTrace.addEvent("iterable next: " + name, traceName); } Object value = i.next(); if (trace) { event.end(); } Scope scope; if (!(value instanceof Boolean)) { scope = new Scope(value, s); } else { scope = new Scope(s); } scope.put(name, value); return scope; } public void remove() { throw new UnsupportedOperationException(); } }; } }; } // Mustache template function cache size. For i18n should be > #i18n stanzas * languages private static final int SIZE = Integer.getInteger("mustache.cachesize", 100000); private static Map templateFunctionCache = new MapMaker().maximumSize(SIZE).makeMap(); /** * Explicit function iteration. * * @param scope * @param f * @return */ public FunctionIterator function(final Scope scope, final Function f) { final boolean templateFunction = f instanceof TemplateFunction; return new FunctionIterator(f) { @Override public Iterator iterator() { return new Iterator() { boolean first = true; StringWriter writer = new StringWriter(); @Override public synchronized boolean hasNext() { if (first) { capturedWriter.get().push(new FutureWriter(writer)); } else { try { capturedWriter.get().pop().flush(); Object apply = f.apply(writer.toString()); String applyString = apply == null ? null : String.valueOf(apply); if (templateFunction) { if (applyString != null) { Mustache mustache = templateFunctionCache.get(applyString); if (mustache == null) { mustache = mj.parse(applyString, name); templateFunctionCache.put(applyString, mustache); } mustache.execute(actual.get().pop(), scope); } } else { actual.get().pop().write(applyString); } } catch (Exception e) { throw new RuntimeException(e); } } return first; } @Override public synchronized Scope next() { if (first) { first = false; return scope; } throw new NoSuchElementException(); } @Override public void remove() { } }; } }; } /** * Set the mustache creator. * * @param mj */ public void setMustacheJava(MustacheJava mj) { this.mj = mj; } /** * Compile a partial in the context of this mustache. * * @param name * @return * @throws MustacheException */ public Mustache partial(String name) throws MustacheException { return compilePartial(name); } protected Mustache compilePartial(String name) throws MustacheException { MustacheTrace.Event event = null; if (trace) { event = MustacheTrace.addEvent("compile partial: " + name, ""); } Mustache mustache; String partialExtension = getPartialExtension(); String partialName = "".equals(partialExtension) ? name : name + "." + partialExtension; mustache = mj.parseFile(partialName); mustache.setMustacheJava(mj); if (trace) { event.end(); } return mustache; } protected String getPartialExtension() { int index = name.lastIndexOf("."); return name.substring(index + 1); } /** * Execute a partial in the context of this mustache. * * @param writer * @param s * @param name * @param partial * @throws MustacheException */ public void partial(FutureWriter writer, Scope s, final String name, Mustache partial) throws MustacheException { if (name != null) { MustacheTrace.Event event = null; if (trace) { Object parent = s.getParent(); String traceName = parent == null ? s.getClass().getName() : parent.getClass().getName(); event = MustacheTrace.addEvent("partial: " + name, traceName); } Object parent = s.get(name); Scope scope = parent == null ? s : new Scope(parent, s); partial.execute(writer, scope); if (trace) { event.end(); } } } /** * Inversion * * @param s * @param name * @return */ public Iterable inverted(final Scope s, final String name) { MustacheTrace.Event event = null; if (trace) { Object parent = s.getParent(); String traceName = parent == null ? s.getClass().getName() : parent.getClass().getName(); event = MustacheTrace.addEvent("inverted: " + name, traceName); } Object possibleFuture = getValue(s, name); while (possibleFuture instanceof Future) { try { possibleFuture = ((Future) possibleFuture).get(); } catch (Exception e) { throw new RuntimeException(e); } } final Object value = possibleFuture; if (trace) { event.end(); } boolean isntEmpty = value instanceof Iterable && ((Iterable) value).iterator().hasNext(); if (isntEmpty || (value instanceof Boolean && ((Boolean) value)) || (value != null && !(value instanceof Iterable) && !(value instanceof Boolean))) { return EMPTY; } final Scope scope = new Scope(s); scope.put(name, true); return new Iterable() { @Override public Iterator iterator() { return new Iterator() { Scope value = scope; @Override public boolean hasNext() { return value != null; } @Override public Scope next() { Scope tmp = value; value = null; return tmp; } @Override public void remove() { } }; } }; } // Don't over report missing values private static Map missing = new ConcurrentHashMap(); /** * Get a value from the scope. * * @param s * @param name * @return */ public Object getValue(Scope s, String name) { try { Object o; // TODO: If we get the implicit current element, we just grab the // first value from the current scope. This is somewhat dangerous // if used in the wrong scope or if some additional values have // been added to the scope. We might want to figure out a more robust // way to implement this. If you do not use "." it won't matter if (IMPLICIT_CURRENT_ELEMENT_TOKEN.equals(name)) { o = s.values().iterator().next(); } else { o = s.get(name); } if (o == null && debug) { StringBuilder sb = new StringBuilder(); for (StackTraceElement ste : Thread.currentThread().getStackTrace()) { String className = ste.getClassName(); if (className.startsWith("com.sampullara.mustaches.Mustache")) { sb.append(this.name).append(":").append(ste.getLineNumber()); break; } } if (sb.length() == 0) { sb.append(this.name).append(":").append(line.get()); } String location = name + " @ " + sb; if (!name.startsWith("_") && missing.put(location, true) == null) { logger.warning("No field, method or key found for: " + location + " " + s); } } if (o == NULL) { return null; } return o; } catch (Exception e) { throw new RuntimeException(e); } } private static Pattern escapedPattern = Pattern.compile("^&\\w+;"); // Override this in a super class if you don't want encoding or would like // to change the way encoding works. Also, if you use unexecute, make sure // also do the inverse in decode. public String encode(String value) { StringBuilder sb = new StringBuilder(); int position = 0; int length = value.length(); for (int i = 0; i < length; i++) { char c = value.charAt(i); switch (c) { case '&': if (!escapedPattern.matcher(value.substring(i, length)).find()) { sb.append(value, position, i); position = i + 1; sb.append("&"); } else { if (position != 0) { sb.append(value, position, i); position = i + 1; sb.append("&"); } } break; case '\\': sb.append(value, position, i); position = i + 1; sb.append("\\\\"); break; case '"': sb.append(value, position, i); position = i + 1; sb.append("""); break; case '<': sb.append(value, position, i); position = i + 1; sb.append("<"); break; case '>': sb.append(value, position, i); position = i + 1; sb.append(">"); break; case '\n': sb.append(value, position, i); position = i + 1; sb.append(" "); break; } } if (position == 0) { return value; } else { sb.append(value, position, value.length()); return sb.toString(); } } }