diff --git a/src/main/java/com/samskivert/mustache/Mustache.java b/src/main/java/com/samskivert/mustache/Mustache.java index b9a9e39..cc53473 100644 --- a/src/main/java/com/samskivert/mustache/Mustache.java +++ b/src/main/java/com/samskivert/mustache/Mustache.java @@ -251,7 +251,7 @@ public class Mustache public StringSegment (String text) { _text = text; } - @Override public void execute (Object ctx, Writer out) { + @Override public void execute (Template tmpl, Object ctx, Writer out) { write(out, _text); } protected final String _text; @@ -271,8 +271,8 @@ public class Mustache super(name); _escapeHTML = escapeHTML; } - @Override public void execute (Object ctx, Writer out) { - Object value = getValue(ctx, _name); + @Override public void execute (Template tmpl, Object ctx, Writer out) { + Object value = tmpl.getValue(ctx, _name); // TODO: configurable behavior on missing values if (value != null) { String text = String.valueOf(value); @@ -288,9 +288,9 @@ public class Mustache super(name); _segs = segs; } - protected void executeSegs (Object ctx, Writer out) { + protected void executeSegs (Template tmpl, Object ctx, Writer out) { for (Template.Segment seg : _segs) { - seg.execute(ctx, out); + seg.execute(tmpl, ctx, out); } } protected final Template.Segment[] _segs; @@ -301,31 +301,31 @@ public class Mustache public SectionSegment (String name, Template.Segment[] segs) { super(name, segs); } - @Override public void execute (Object ctx, Writer out) { - Object value = getValue(ctx, _name); + @Override public void execute (Template tmpl, Object ctx, Writer out) { + Object value = tmpl.getValue(ctx, _name); if (value == null) { return; // TODO: configurable behavior on missing values } if (value instanceof Iterable) { Iterable iable = (Iterable)value; for (Object elem : iable) { - executeSegs(elem, out); + executeSegs(tmpl, elem, out); } } else if (value instanceof Boolean) { if ((Boolean)value) { - executeSegs(ctx, out); + executeSegs(tmpl, ctx, out); } } else if (value.getClass().isArray()) { for (int ii = 0, ll = Array.getLength(value); ii < ll; ii++) { - executeSegs(Array.get(value, ii), out); + executeSegs(tmpl, Array.get(value, ii), out); } } else if (value instanceof Iterator) { Iterator iter = (Iterator)value; while (iter.hasNext()) { - executeSegs(iter.next(), out); + executeSegs(tmpl, iter.next(), out); } } else { - executeSegs(value, out); + executeSegs(tmpl, value, out); } } } @@ -335,28 +335,28 @@ public class Mustache public InvertedSectionSegment (String name, Template.Segment[] segs) { super(name, segs); } - @Override public void execute (Object ctx, Writer out) { - Object value = getValue(ctx, _name); + @Override public void execute (Template tmpl, Object ctx, Writer out) { + Object value = tmpl.getValue(ctx, _name); if (value == null) { - executeSegs(ctx, out); // TODO: configurable behavior on missing values + executeSegs(tmpl, ctx, out); // TODO: configurable behavior on missing values } if (value instanceof Iterable) { Iterable iable = (Iterable)value; if (!iable.iterator().hasNext()) { - executeSegs(ctx, out); + executeSegs(tmpl, ctx, out); } } else if (value instanceof Boolean) { if (!(Boolean)value) { - executeSegs(ctx, out); + executeSegs(tmpl, ctx, out); } } else if (value.getClass().isArray()) { if (Array.getLength(value) == 0) { - executeSegs(ctx, out); + executeSegs(tmpl, ctx, out); } } else if (value instanceof Iterator) { Iterator iter = (Iterator)value; if (!iter.hasNext()) { - executeSegs(ctx, out); + executeSegs(tmpl, ctx, out); } } } diff --git a/src/main/java/com/samskivert/mustache/Template.java b/src/main/java/com/samskivert/mustache/Template.java index f3e5a03..3123233 100644 --- a/src/main/java/com/samskivert/mustache/Template.java +++ b/src/main/java/com/samskivert/mustache/Template.java @@ -6,10 +6,13 @@ package com.samskivert.mustache; import java.io.IOException; 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; /** * Represents a compiled template. @@ -42,7 +45,7 @@ public class Template public void execute (Object data, Writer out) throws MustacheException { for (Segment seg : _segs) { - seg.execute(data, out); + seg.execute(this, data, out); } } @@ -64,15 +67,124 @@ public class Template _segs = segs; } + /** + * Called by executing segments to obtain the value of the specified variable in the supplied + * context. + */ + protected Object getValue (Object ctx, String name) + { + if (ctx == null) { + throw new NullPointerException("Null context for variable '" + name + "'"); + } + + Key key = new Key(ctx.getClass(), name); + VariableFetcher fetcher = _fcache.get(key); + if (fetcher != null) { + try { + return fetcher.get(ctx, name); + } catch (Exception e) { + // zoiks! non-monomorphic call site, update the cache and try again + _fcache.put(key, fetcher = createFetcher(key)); + } + } else { + fetcher = createFetcher(key); + } + + try { + Object value = fetcher.get(ctx, name); + _fcache.put(key, fetcher); + return value; + } catch (Exception e) { + throw new MustacheException("Failure fetching variable '" + name + "'", e); + } + } + + protected final Segment[] _segs; + protected final Map _fcache = + new ConcurrentHashMap(); + + protected static VariableFetcher createFetcher (Key key) + { + if (Map.class.isAssignableFrom(key.cclass)) { + return MAP_FETCHER; + } + + final Method m = getMethod(key.cclass, key.name); + if (m != null) { + return new VariableFetcher() { + public Object get (Object ctx, String name) throws Exception { + return m.invoke(ctx); + } + }; + } + + final Field f = getField(key.cclass, key.name); + if (f != null) { + return new VariableFetcher() { + public Object get (Object ctx, String name) throws Exception { + return f.get(ctx); + } + }; + } + + throw new MustacheException("No method or field with appropriate name and not Map " + + "[var=" + key.name + ", ctx=" + key.cclass.getName() + "]"); + } + + protected static Method getMethod (Class clazz, String name) + { + Method m; + try { + m = clazz.getDeclaredMethod(name); + if (!m.isAccessible()) { + m.setAccessible(true); + } + return m; + } catch (Exception e) { + // fall through + } + try { + m = clazz.getDeclaredMethod( + "get" + Character.toUpperCase(name.charAt(0)) + name.substring(1)); + if (!m.isAccessible()) { + m.setAccessible(true); + } + return m; + } catch (Exception e) { + // fall through + } + + Class sclass = clazz.getSuperclass(); + if (sclass != Object.class && sclass != null) { + return getMethod(clazz.getSuperclass(), name); + } + return null; + } + + protected static Field getField (Class clazz, String name) + { + Field f; + try { + f = clazz.getDeclaredField(name); + if (!f.isAccessible()) { + f.setAccessible(true); + } + return f; + } catch (Exception e) { + // fall through + } + + Class sclass = clazz.getSuperclass(); + if (sclass != Object.class && sclass != null) { + return getField(clazz.getSuperclass(), name); + } + return null; + } + /** A template is broken into segments. */ protected static abstract class Segment { - abstract void execute (Object ctx, Writer out); - - protected Object getValue (Object ctx, String name) { - // TODO: support things other than values - return ((Map)ctx).get(name); - } + abstract void execute (Template tmpl, Object ctx, Writer out); protected static void write (Writer out, String data) { try { @@ -83,5 +195,34 @@ public class Template } } - protected final Segment[] _segs; + /** 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.intern(); + } + + @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 abstract class VariableFetcher { + abstract Object get (Object ctx, String name) throws Exception; + } + + protected static final VariableFetcher MAP_FETCHER = new VariableFetcher() { + public Object get (Object ctx, String name) throws Exception { + return ((Map)ctx).get(name); + } + }; } diff --git a/src/test/java/com/samskivert/mustache/MustacheTest.java b/src/test/java/com/samskivert/mustache/MustacheTest.java index 85cfb6b..b07c639 100644 --- a/src/test/java/com/samskivert/mustache/MustacheTest.java +++ b/src/test/java/com/samskivert/mustache/MustacheTest.java @@ -17,57 +17,98 @@ import static org.junit.Assert.*; public class MustacheTest { @Test public void testSimpleVariable () { - test("bar", "{{foo}}", "foo", "bar"); + test("bar", "{{foo}}", context("foo", "bar")); + } + + @Test public void testFieldVariable () { + test("bar", "{{foo}}", new Object() { + String foo = "bar"; + }); + } + + @Test public void testMethodVariable () { + test("bar", "{{foo}}", new Object() { + String foo () { return "bar"; } + }); + } + + @Test public void testPropertyVariable () { + test("bar", "{{foo}}", new Object() { + String getFoo () { return "bar"; } + }); + } + + @Test public void testCallSiteReuse () { + Template tmpl = Mustache.compile("{{foo}}"); + Object ctx = new Object() { + String getFoo () { return "bar"; } + }; + for (int ii = 0; ii < 50; ii++) { + assertEquals("bar", tmpl.execute(ctx)); + } + } + + @Test public void testCallSiteChange () { + Template tmpl = Mustache.compile("{{foo}}"); + assertEquals("bar", tmpl.execute(new Object() { + String getFoo () { return "bar"; } + })); + assertEquals("bar", tmpl.execute(new Object() { + String foo = "bar"; + })); } @Test public void testOneShotSection () { - test("baz", "{{#foo}}{{bar}}{{/foo}}", "foo", context("bar", "baz")); + test("baz", "{{#foo}}{{bar}}{{/foo}}", context("foo", context("bar", "baz"))); } @Test public void testListSection () { - test("bazbif", "{{#foo}}{{bar}}{{/foo}}", "foo", - Arrays.asList(context("bar", "baz"), context("bar", "bif"))); + test("bazbif", "{{#foo}}{{bar}}{{/foo}}", context( + "foo", Arrays.asList(context("bar", "baz"), context("bar", "bif")))); } @Test public void testArraySection () { - test("bazbif", "{{#foo}}{{bar}}{{/foo}}", "foo", - new Object[] { context("bar", "baz"), context("bar", "bif") }); + test("bazbif", "{{#foo}}{{bar}}{{/foo}}", + context("foo", new Object[] { + context("bar", "baz"), context("bar", "bif") })); } @Test public void testIteratorSection () { - test("bazbif", "{{#foo}}{{bar}}{{/foo}}", "foo", - Arrays.asList(context("bar", "baz"), context("bar", "bif")).iterator()); + test("bazbif", "{{#foo}}{{bar}}{{/foo}}", + context("foo", Arrays.asList(context("bar", "baz"), + context("bar", "bif")).iterator())); } @Test public void testEmptyListSection () { - test("", "{{#foo}}{{bar}}{{/foo}}", "foo", Collections.emptyList()); + test("", "{{#foo}}{{bar}}{{/foo}}", context("foo", Collections.emptyList())); } @Test public void testEmptyArraySection () { - test("", "{{#foo}}{{bar}}{{/foo}}", "foo", new Object[0]); + test("", "{{#foo}}{{bar}}{{/foo}}", context("foo", new Object[0])); } @Test public void testEmptyIteratorSection () { - test("", "{{#foo}}{{bar}}{{/foo}}", "foo", Collections.emptyList().iterator()); + test("", "{{#foo}}{{bar}}{{/foo}}", context("foo", Collections.emptyList().iterator())); } @Test public void testFalseSection () { - test("", "{{#foo}}{{bar}}{{/foo}}", "foo", false); + test("", "{{#foo}}{{bar}}{{/foo}}", context("foo", false)); } @Test public void testNestedListSection () { - test("1234", "{{#a}}{{#b}}{{c}}{{/b}}{{#d}}{{e}}{{/d}}{{/a}}", "a", - new Object[] { context("b", new Object[] { context("c", "1"), context("c", "2") }), - context("d", new Object[] { context("e", "3"), context("e", "4") }) }); + 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") }) })); } @Test public void testComment () { - test("foobar", "foo{{! nothing to see here}}bar"); + test("foobar", "foo{{! nothing to see here}}bar", new Object()); } - protected void test (String expected, String template, Object... data) + protected void test (String expected, String template, Object ctx) { - assertEquals(expected, Mustache.compile(template).execute(context(data))); + assertEquals(expected, Mustache.compile(template).execute(ctx)); } protected static Object context (Object... data)