diff --git a/src/main/java/com/samskivert/mustache/Mustache.java b/src/main/java/com/samskivert/mustache/Mustache.java index bafbfc9..40844af 100644 --- a/src/main/java/com/samskivert/mustache/Mustache.java +++ b/src/main/java/com/samskivert/mustache/Mustache.java @@ -18,7 +18,7 @@ import java.util.Map; *

Basic usage:

{@code
  * String source = "Hello {{arg}}!";
  * Template tmpl = Mustache.compiler().compile(source);
- * Map context = new HashMap();
+ * Map context = new HashMap();
  * context.put("arg", "world");
  * tmpl.execute(context); // returns "Hello world!" }
*

Limitations: @@ -315,7 +315,7 @@ public class Mustache public StringSegment (String text) { _text = text; } - @Override public void execute (Template tmpl, Object ctx, Writer out) { + @Override public void execute (Template tmpl, Template.Context ctx, Writer out) { write(out, _text); } protected final String _text; @@ -335,7 +335,7 @@ public class Mustache super(name); _escapeHTML = escapeHTML; } - @Override public void execute (Template tmpl, Object ctx, Writer out) { + @Override public void execute (Template tmpl, Template.Context ctx, Writer out) { Object value = tmpl.getValue(ctx, _name); // TODO: configurable behavior on missing values if (value != null) { @@ -352,7 +352,7 @@ public class Mustache super(name); _segs = segs; } - protected void executeSegs (Template tmpl, Object ctx, Writer out) { + protected void executeSegs (Template tmpl, Template.Context ctx, Writer out) { for (Template.Segment seg : _segs) { seg.execute(tmpl, ctx, out); } @@ -365,7 +365,7 @@ public class Mustache public SectionSegment (String name, Template.Segment[] segs) { super(name, segs); } - @Override public void execute (Template tmpl, Object ctx, Writer out) { + @Override public void execute (Template tmpl, Template.Context ctx, Writer out) { Object value = tmpl.getValue(ctx, _name); if (value == null) { return; // TODO: configurable behavior on missing values @@ -373,7 +373,7 @@ public class Mustache if (value instanceof Iterable) { Iterable iable = (Iterable)value; for (Object elem : iable) { - executeSegs(tmpl, elem, out); + executeSegs(tmpl, ctx.nest(elem), out); } } else if (value instanceof Boolean) { if ((Boolean)value) { @@ -381,15 +381,15 @@ public class Mustache } } else if (value.getClass().isArray()) { for (int ii = 0, ll = Array.getLength(value); ii < ll; ii++) { - executeSegs(tmpl, Array.get(value, ii), out); + executeSegs(tmpl, ctx.nest(Array.get(value, ii)), out); } } else if (value instanceof Iterator) { Iterator iter = (Iterator)value; while (iter.hasNext()) { - executeSegs(tmpl, iter.next(), out); + executeSegs(tmpl, ctx.nest(iter.next()), out); } } else { - executeSegs(tmpl, value, out); + executeSegs(tmpl, ctx.nest(value), out); } } } @@ -399,7 +399,7 @@ public class Mustache public InvertedSectionSegment (String name, Template.Segment[] segs) { super(name, segs); } - @Override public void execute (Template tmpl, Object ctx, Writer out) { + @Override public void execute (Template tmpl, Template.Context ctx, Writer out) { Object value = tmpl.getValue(ctx, _name); if (value == null) { executeSegs(tmpl, ctx, out); // TODO: configurable behavior on missing values diff --git a/src/main/java/com/samskivert/mustache/Template.java b/src/main/java/com/samskivert/mustache/Template.java index 649fc55..6277337 100644 --- a/src/main/java/com/samskivert/mustache/Template.java +++ b/src/main/java/com/samskivert/mustache/Template.java @@ -8,9 +8,7 @@ import java.io.StringWriter; import java.io.Writer; import java.lang.reflect.Field; import java.lang.reflect.Method; -import java.util.Deque; import java.util.Iterator; -import java.util.LinkedList; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -44,8 +42,9 @@ public class Template */ public void execute (Object context, Writer out) throws MustacheException { + Context ctx = new Context(context, null); for (Segment seg : _segs) { - seg.execute(this, context, out); + seg.execute(this, ctx, out); } } @@ -69,9 +68,22 @@ public class Template * Called by executing segments to obtain the value of the specified variable in the supplied * context. */ - protected Object getValue (Object ctx, String name) + protected Object getValue (Context ctx, String name) { - if (ctx == null) { + while (ctx != null) { + Object value = getValueIn(ctx.data, name); + if (value != null) { + return value; + } + ctx = ctx.parent; + } + // we've popped all the way off the top of our stack of contexts, so fail + throw new MustacheException("No key, method or field with name '" + name + "'"); + } + + protected Object getValueIn (Object data, String name) + { + if (data == null) { throw new NullPointerException("Null context for variable '" + name + "'"); } @@ -79,26 +91,34 @@ public class Template // resolve the subsequent component and so forth if (name.indexOf(".") != -1) { for (String comp : name.split("\\.")) { - ctx = getValue(ctx, comp); + // 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, comp); } - return ctx; + return data; } - Key key = new Key(ctx.getClass(), name); + Key key = new Key(data.getClass(), name); VariableFetcher fetcher = _fcache.get(key); if (fetcher != null) { try { - return fetcher.get(ctx, name); + return fetcher.get(data, name); } catch (Exception e) { // zoiks! non-monomorphic call site, update the cache and try again - _fcache.put(key, fetcher = createFetcher(key)); + fetcher = createFetcher(key); } } else { fetcher = createFetcher(key); } + // 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 null; + } + try { - Object value = fetcher.get(ctx, name); + Object value = fetcher.get(data, name); _fcache.put(key, fetcher); return value; } catch (Exception e) { @@ -138,8 +158,7 @@ public class Template }; } - throw new MustacheException("No method or field with appropriate name and not Map " + - "[var=" + key.name + ", ctx=" + key.cclass.getName() + "]"); + return null; } protected static Method getMethod (Class clazz, String name) @@ -196,10 +215,25 @@ public class Template return null; } + protected static class Context + { + public final Object data; + public final Context parent; + + public Context (Object data, Context parent) { + this.data = data; + this.parent = parent; + } + + public Context nest (Object data) { + return new Context(data, this); + } + } + /** A template is broken into segments. */ protected static abstract class Segment { - abstract void execute (Template tmpl, Object ctx, Writer out); + abstract void execute (Template tmpl, Context ctx, Writer out); protected static void write (Writer out, String data) { try { diff --git a/src/test/java/com/samskivert/mustache/MustacheTest.java b/src/test/java/com/samskivert/mustache/MustacheTest.java index fff9908..895cd36 100644 --- a/src/test/java/com/samskivert/mustache/MustacheTest.java +++ b/src/test/java/com/samskivert/mustache/MustacheTest.java @@ -104,9 +104,8 @@ public class MustacheTest @Test public void testNestedListSection () { test("1234", "{{#a}}{{#b}}{{c}}{{/b}}{{#d}}{{e}}{{/d}}{{/a}}", - context("a", new Object[] { - context("b", new Object[] { context("c", "1"), context("c", "2") }), - context("d", new Object[] { context("e", "3"), context("e", "4") }) })); + context("a", context("b", new Object[] { context("c", "1"), context("c", "2") }, + "d", new Object[] { context("e", "3"), context("e", "4") }))); } @Test public void testComment () { @@ -166,6 +165,19 @@ public class MustacheTest "endlist", tmpl, context("items", Collections.emptyList())); } + @Test public void testNestedContexts () { + test("foo((foobar)(foobaz))", "{{name}}({{#things}}({{name}}{{thing_name}}){{/things}})", + context("name", "foo", + "things", Arrays.asList(context("thing_name", "bar"), + context("thing_name", "baz")))); + } + + @Test public void testShadowedContext () { + test("foo((bar)(baz))", "{{name}}({{#things}}({{name}}){{/things}})", + context("name", "foo", + "things", Arrays.asList(context("name", "bar"), context("name", "baz")))); + } + protected void test (String expected, String template, Object ctx) { assertEquals(expected, Mustache.compiler().compile(template).execute(ctx));