Added support for obtaining variable values from fields and methods. Added a
template-local variable fetching cache that avoids reresolving the source of a variable when it remains monomorphic (e.g. it's always a field of class Foo.class or always a method named getFoo(), etc.).
This commit is contained in:
@@ -251,7 +251,7 @@ public class Mustache
|
|||||||
public StringSegment (String text) {
|
public StringSegment (String text) {
|
||||||
_text = text;
|
_text = text;
|
||||||
}
|
}
|
||||||
@Override public void execute (Object ctx, Writer out) {
|
@Override public void execute (Template tmpl, Object ctx, Writer out) {
|
||||||
write(out, _text);
|
write(out, _text);
|
||||||
}
|
}
|
||||||
protected final String _text;
|
protected final String _text;
|
||||||
@@ -271,8 +271,8 @@ public class Mustache
|
|||||||
super(name);
|
super(name);
|
||||||
_escapeHTML = escapeHTML;
|
_escapeHTML = escapeHTML;
|
||||||
}
|
}
|
||||||
@Override public void execute (Object ctx, Writer out) {
|
@Override public void execute (Template tmpl, Object ctx, Writer out) {
|
||||||
Object value = getValue(ctx, _name);
|
Object value = tmpl.getValue(ctx, _name);
|
||||||
// TODO: configurable behavior on missing values
|
// TODO: configurable behavior on missing values
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
String text = String.valueOf(value);
|
String text = String.valueOf(value);
|
||||||
@@ -288,9 +288,9 @@ public class Mustache
|
|||||||
super(name);
|
super(name);
|
||||||
_segs = segs;
|
_segs = segs;
|
||||||
}
|
}
|
||||||
protected void executeSegs (Object ctx, Writer out) {
|
protected void executeSegs (Template tmpl, Object ctx, Writer out) {
|
||||||
for (Template.Segment seg : _segs) {
|
for (Template.Segment seg : _segs) {
|
||||||
seg.execute(ctx, out);
|
seg.execute(tmpl, ctx, out);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
protected final Template.Segment[] _segs;
|
protected final Template.Segment[] _segs;
|
||||||
@@ -301,31 +301,31 @@ public class Mustache
|
|||||||
public SectionSegment (String name, Template.Segment[] segs) {
|
public SectionSegment (String name, Template.Segment[] segs) {
|
||||||
super(name, segs);
|
super(name, segs);
|
||||||
}
|
}
|
||||||
@Override public void execute (Object ctx, Writer out) {
|
@Override public void execute (Template tmpl, Object ctx, Writer out) {
|
||||||
Object value = getValue(ctx, _name);
|
Object value = tmpl.getValue(ctx, _name);
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return; // TODO: configurable behavior on missing values
|
return; // TODO: configurable behavior on missing values
|
||||||
}
|
}
|
||||||
if (value instanceof Iterable<?>) {
|
if (value instanceof Iterable<?>) {
|
||||||
Iterable<?> iable = (Iterable<?>)value;
|
Iterable<?> iable = (Iterable<?>)value;
|
||||||
for (Object elem : iable) {
|
for (Object elem : iable) {
|
||||||
executeSegs(elem, out);
|
executeSegs(tmpl, elem, out);
|
||||||
}
|
}
|
||||||
} else if (value instanceof Boolean) {
|
} else if (value instanceof Boolean) {
|
||||||
if ((Boolean)value) {
|
if ((Boolean)value) {
|
||||||
executeSegs(ctx, out);
|
executeSegs(tmpl, ctx, out);
|
||||||
}
|
}
|
||||||
} else if (value.getClass().isArray()) {
|
} else if (value.getClass().isArray()) {
|
||||||
for (int ii = 0, ll = Array.getLength(value); ii < ll; ii++) {
|
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<?>) {
|
} else if (value instanceof Iterator<?>) {
|
||||||
Iterator<?> iter = (Iterator<?>)value;
|
Iterator<?> iter = (Iterator<?>)value;
|
||||||
while (iter.hasNext()) {
|
while (iter.hasNext()) {
|
||||||
executeSegs(iter.next(), out);
|
executeSegs(tmpl, iter.next(), out);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
executeSegs(value, out);
|
executeSegs(tmpl, value, out);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -335,28 +335,28 @@ public class Mustache
|
|||||||
public InvertedSectionSegment (String name, Template.Segment[] segs) {
|
public InvertedSectionSegment (String name, Template.Segment[] segs) {
|
||||||
super(name, segs);
|
super(name, segs);
|
||||||
}
|
}
|
||||||
@Override public void execute (Object ctx, Writer out) {
|
@Override public void execute (Template tmpl, Object ctx, Writer out) {
|
||||||
Object value = getValue(ctx, _name);
|
Object value = tmpl.getValue(ctx, _name);
|
||||||
if (value == null) {
|
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<?>) {
|
if (value instanceof Iterable<?>) {
|
||||||
Iterable<?> iable = (Iterable<?>)value;
|
Iterable<?> iable = (Iterable<?>)value;
|
||||||
if (!iable.iterator().hasNext()) {
|
if (!iable.iterator().hasNext()) {
|
||||||
executeSegs(ctx, out);
|
executeSegs(tmpl, ctx, out);
|
||||||
}
|
}
|
||||||
} else if (value instanceof Boolean) {
|
} else if (value instanceof Boolean) {
|
||||||
if (!(Boolean)value) {
|
if (!(Boolean)value) {
|
||||||
executeSegs(ctx, out);
|
executeSegs(tmpl, ctx, out);
|
||||||
}
|
}
|
||||||
} else if (value.getClass().isArray()) {
|
} else if (value.getClass().isArray()) {
|
||||||
if (Array.getLength(value) == 0) {
|
if (Array.getLength(value) == 0) {
|
||||||
executeSegs(ctx, out);
|
executeSegs(tmpl, ctx, out);
|
||||||
}
|
}
|
||||||
} else if (value instanceof Iterator<?>) {
|
} else if (value instanceof Iterator<?>) {
|
||||||
Iterator<?> iter = (Iterator<?>)value;
|
Iterator<?> iter = (Iterator<?>)value;
|
||||||
if (!iter.hasNext()) {
|
if (!iter.hasNext()) {
|
||||||
executeSegs(ctx, out);
|
executeSegs(tmpl, ctx, out);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,13 @@ package com.samskivert.mustache;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.StringWriter;
|
import java.io.StringWriter;
|
||||||
import java.io.Writer;
|
import java.io.Writer;
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
import java.util.Deque;
|
import java.util.Deque;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a compiled template.
|
* Represents a compiled template.
|
||||||
@@ -42,7 +45,7 @@ public class Template
|
|||||||
public void execute (Object data, Writer out) throws MustacheException
|
public void execute (Object data, Writer out) throws MustacheException
|
||||||
{
|
{
|
||||||
for (Segment seg : _segs) {
|
for (Segment seg : _segs) {
|
||||||
seg.execute(data, out);
|
seg.execute(this, data, out);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,15 +67,124 @@ public class Template
|
|||||||
_segs = segs;
|
_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<Key, VariableFetcher> _fcache =
|
||||||
|
new ConcurrentHashMap<Key, VariableFetcher>();
|
||||||
|
|
||||||
|
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. */
|
/** A template is broken into segments. */
|
||||||
protected static abstract class Segment
|
protected static abstract class Segment
|
||||||
{
|
{
|
||||||
abstract void execute (Object ctx, Writer out);
|
abstract void execute (Template tmpl, Object ctx, Writer out);
|
||||||
|
|
||||||
protected Object getValue (Object ctx, String name) {
|
|
||||||
// TODO: support things other than values
|
|
||||||
return ((Map<?,?>)ctx).get(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected static void write (Writer out, String data) {
|
protected static void write (Writer out, String data) {
|
||||||
try {
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,57 +17,98 @@ import static org.junit.Assert.*;
|
|||||||
public class MustacheTest
|
public class MustacheTest
|
||||||
{
|
{
|
||||||
@Test public void testSimpleVariable () {
|
@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 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 public void testListSection () {
|
||||||
test("bazbif", "{{#foo}}{{bar}}{{/foo}}", "foo",
|
test("bazbif", "{{#foo}}{{bar}}{{/foo}}", context(
|
||||||
Arrays.asList(context("bar", "baz"), context("bar", "bif")));
|
"foo", Arrays.asList(context("bar", "baz"), context("bar", "bif"))));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test public void testArraySection () {
|
@Test public void testArraySection () {
|
||||||
test("bazbif", "{{#foo}}{{bar}}{{/foo}}", "foo",
|
test("bazbif", "{{#foo}}{{bar}}{{/foo}}",
|
||||||
new Object[] { context("bar", "baz"), context("bar", "bif") });
|
context("foo", new Object[] {
|
||||||
|
context("bar", "baz"), context("bar", "bif") }));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test public void testIteratorSection () {
|
@Test public void testIteratorSection () {
|
||||||
test("bazbif", "{{#foo}}{{bar}}{{/foo}}", "foo",
|
test("bazbif", "{{#foo}}{{bar}}{{/foo}}",
|
||||||
Arrays.asList(context("bar", "baz"), context("bar", "bif")).iterator());
|
context("foo", Arrays.asList(context("bar", "baz"),
|
||||||
|
context("bar", "bif")).iterator()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test public void testEmptyListSection () {
|
@Test public void testEmptyListSection () {
|
||||||
test("", "{{#foo}}{{bar}}{{/foo}}", "foo", Collections.emptyList());
|
test("", "{{#foo}}{{bar}}{{/foo}}", context("foo", Collections.emptyList()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test public void testEmptyArraySection () {
|
@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 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 public void testFalseSection () {
|
||||||
test("", "{{#foo}}{{bar}}{{/foo}}", "foo", false);
|
test("", "{{#foo}}{{bar}}{{/foo}}", context("foo", false));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test public void testNestedListSection () {
|
@Test public void testNestedListSection () {
|
||||||
test("1234", "{{#a}}{{#b}}{{c}}{{/b}}{{#d}}{{e}}{{/d}}{{/a}}", "a",
|
test("1234", "{{#a}}{{#b}}{{c}}{{/b}}{{#d}}{{e}}{{/d}}{{/a}}",
|
||||||
new Object[] { context("b", new Object[] { context("c", "1"), context("c", "2") }),
|
context("a", new Object[] {
|
||||||
context("d", new Object[] { context("e", "3"), context("e", "4") }) });
|
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 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)
|
protected static Object context (Object... data)
|
||||||
|
|||||||
Reference in New Issue
Block a user