Extracted the iteration over collections and creation of variable fetchers into
a pluggable component. This allows scaling down to the GWT environment by hiding the reflection code via GWT's super source mechanism. And it allows scaling up to non-Java languages (like Scala) by allowing the library client to extend the DefaultCollector and teach it about Scala Iterators, Iterables and Maps. I also just realized that GWT has no facilities for Reader and Writer, which is going to complicate making things work in GWT even further. But for now, I'll just leave things as is.
This commit is contained in:
@@ -62,6 +62,9 @@
|
||||
<showWarnings>true</showWarnings>
|
||||
<!-- yes, those quoted spaces are a workaround sanctioned by the Maven idiocracy -->
|
||||
<compilerArgument>-Xlint" "-Xlint:-serial" "-Xlint:-path</compilerArgument>
|
||||
<excludes>
|
||||
<exclude>com/samskivert/mustache/gwt/**</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.samskivert.mustache;
|
||||
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* A collector that does not use reflection and can be used in GWT.
|
||||
*/
|
||||
public class BasicCollector implements Mustache.Collector
|
||||
{
|
||||
@Override
|
||||
public Iterator<?> toIterator (final Object value) {
|
||||
if (value instanceof Iterable<?>) {
|
||||
return ((Iterable<?>)value).iterator();
|
||||
}
|
||||
if (value instanceof Iterator<?>) {
|
||||
return (Iterator<?>)value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mustache.VariableFetcher createFetcher (Class<?> cclass, String name)
|
||||
{
|
||||
// support both .name and this.name to fetch members
|
||||
if (name == Template.DOT_NAME || name == Template.THIS_NAME) {
|
||||
return THIS_FETCHER;
|
||||
}
|
||||
|
||||
if (Map.class.isAssignableFrom(cclass)) {
|
||||
return MAP_FETCHER;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected static final Mustache.VariableFetcher MAP_FETCHER = new Mustache.VariableFetcher() {
|
||||
@Override public Object get (Object ctx, String name) throws Exception {
|
||||
return ((Map<?,?>)ctx).get(name);
|
||||
}
|
||||
};
|
||||
|
||||
protected static final Mustache.VariableFetcher THIS_FETCHER = new Mustache.VariableFetcher() {
|
||||
@Override public Object get (Object ctx, String name) throws Exception {
|
||||
return ctx;
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.samskivert.mustache;
|
||||
|
||||
import java.lang.reflect.Array;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
|
||||
import java.util.AbstractList;
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
* The default collector used by JMustache.
|
||||
*/
|
||||
public class DefaultCollector extends BasicCollector
|
||||
{
|
||||
@Override
|
||||
public Iterator<?> toIterator (final Object value) {
|
||||
Iterator<?> iter = super.toIterator(value);
|
||||
if (iter != null) return iter;
|
||||
|
||||
if (value.getClass().isArray()) {
|
||||
return new AbstractList<Object>() {
|
||||
public int size () {
|
||||
return Array.getLength(value);
|
||||
}
|
||||
public Object get (int idx) {
|
||||
return Array.get(value, idx);
|
||||
}
|
||||
}.iterator();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mustache.VariableFetcher createFetcher (Class<?> cclass, String name)
|
||||
{
|
||||
Mustache.VariableFetcher fetcher = super.createFetcher(cclass, name);
|
||||
if (fetcher != null) return fetcher;
|
||||
|
||||
final Method m = getMethod(cclass, name);
|
||||
if (m != null) {
|
||||
return new Mustache.VariableFetcher() {
|
||||
@Override public Object get (Object ctx, String name) throws Exception {
|
||||
return m.invoke(ctx);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
final Field f = getField(cclass, name);
|
||||
if (f != null) {
|
||||
return new Mustache.VariableFetcher() {
|
||||
@Override public Object get (Object ctx, String name) throws Exception {
|
||||
return f.get(ctx);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected Method getMethod (Class<?> clazz, String name)
|
||||
{
|
||||
Method m;
|
||||
try {
|
||||
m = clazz.getDeclaredMethod(name);
|
||||
if (!m.getReturnType().equals(void.class)) {
|
||||
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.getReturnType().equals(void.class)) {
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
@@ -8,24 +8,29 @@ import java.io.IOException;
|
||||
import java.io.Reader;
|
||||
import java.io.StringReader;
|
||||
import java.io.Writer;
|
||||
import java.lang.reflect.Array;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Provides <a href="http://mustache.github.com/">Mustache</a> templating services.
|
||||
* <p> Basic usage: <pre>{@code
|
||||
*
|
||||
* <p> Basic usage:
|
||||
* <pre>{@code
|
||||
* String source = "Hello {{arg}}!";
|
||||
* Template tmpl = Mustache.compiler().compile(source);
|
||||
* Map<String, Object> context = new HashMap<String, Object>();
|
||||
* context.put("arg", "world");
|
||||
* tmpl.execute(context); // returns "Hello world!" }</pre>
|
||||
* tmpl.execute(context); // returns "Hello world!"
|
||||
* }</pre>
|
||||
*
|
||||
* <p> Limitations:
|
||||
* <ul><li> Only one or two character delimiters are supported when using {{=ab cd=}} to change
|
||||
* <ul>
|
||||
* <li> Only one or two character delimiters are supported when using {{=ab cd=}} to change
|
||||
* delimiters.
|
||||
* <li> {{< include}} is not supported. We specifically do not want the complexity of handling the
|
||||
* automatic loading of dependent templates. </ul>
|
||||
* automatic loading of dependent templates.
|
||||
* </ul>
|
||||
*/
|
||||
public class Mustache
|
||||
{
|
||||
@@ -51,6 +56,9 @@ public class Mustache
|
||||
/** The template loader in use during this compilation. */
|
||||
public final TemplateLoader loader;
|
||||
|
||||
/** The collector used by templates compiled with this compiler. */
|
||||
public final Collector collector;
|
||||
|
||||
/** Compiles the supplied template into a repeatedly executable intermediate form. */
|
||||
public Template compile (String template) {
|
||||
return compile(new StringReader(template));
|
||||
@@ -63,16 +71,16 @@ public class Mustache
|
||||
|
||||
/** Returns a compiler that either does or does not escape HTML by default. */
|
||||
public Compiler escapeHTML (boolean escapeHTML) {
|
||||
return new Compiler(escapeHTML, this.standardsMode,
|
||||
this.nullValue, this.missingIsNull, this.loader);
|
||||
return new Compiler(escapeHTML, this.standardsMode, this.nullValue, this.missingIsNull,
|
||||
this.loader, this.collector);
|
||||
}
|
||||
|
||||
/** Returns a compiler that either does or does not use standards mode. Standards mode
|
||||
* disables the non-standard JMustache extensions like looking up missing names in a parent
|
||||
* context. */
|
||||
public Compiler standardsMode (boolean standardsMode) {
|
||||
return new Compiler(this.escapeHTML, standardsMode,
|
||||
this.nullValue, this.missingIsNull, this.loader);
|
||||
return new Compiler(this.escapeHTML, standardsMode, this.nullValue, this.missingIsNull,
|
||||
this.loader, this.collector);
|
||||
}
|
||||
|
||||
/** Returns a compiler that will use the given value for any variable that is missing, or
|
||||
@@ -81,8 +89,8 @@ public class Mustache
|
||||
* regardless of whether a null or default value is configured, if the resolution of part
|
||||
* of a compound key results in a missing or null value, an exception will be raised. */
|
||||
public Compiler defaultValue (String defaultValue) {
|
||||
return new Compiler(this.escapeHTML, this.standardsMode,
|
||||
defaultValue, true, this.loader);
|
||||
return new Compiler(this.escapeHTML, this.standardsMode, defaultValue, true,
|
||||
this.loader, this.collector);
|
||||
}
|
||||
|
||||
/** Returns a compiler that will use the given value for any variable that resolves to
|
||||
@@ -100,23 +108,24 @@ public class Mustache
|
||||
* of part of a compound key results in a missing or null value, an exception will be
|
||||
* raised. */
|
||||
public Compiler nullValue (String nullValue) {
|
||||
return new Compiler(this.escapeHTML, this.standardsMode,
|
||||
nullValue, false, this.loader);
|
||||
return new Compiler(this.escapeHTML, this.standardsMode, nullValue, false,
|
||||
this.loader, this.collector);
|
||||
}
|
||||
|
||||
/** Returns a compiler configured to use the supplied template loader to handle partials. */
|
||||
public Compiler withLoader (TemplateLoader loader) {
|
||||
return new Compiler(this.escapeHTML, this.standardsMode,
|
||||
this.nullValue, this.missingIsNull, loader);
|
||||
return new Compiler(this.escapeHTML, this.standardsMode, this.nullValue,
|
||||
this.missingIsNull, loader, this.collector);
|
||||
}
|
||||
|
||||
protected Compiler (boolean escapeHTML, boolean standardsMode,
|
||||
String nullValue, boolean missingIsNull, TemplateLoader loader) {
|
||||
protected Compiler (boolean escapeHTML, boolean standardsMode, String nullValue,
|
||||
boolean missingIsNull, TemplateLoader loader, Collector collector) {
|
||||
this.escapeHTML = escapeHTML;
|
||||
this.standardsMode = standardsMode;
|
||||
this.nullValue = nullValue;
|
||||
this.missingIsNull = missingIsNull;
|
||||
this.loader = loader;
|
||||
this.collector = collector;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,12 +137,30 @@ public class Mustache
|
||||
public Reader getTemplate (String name) throws Exception;
|
||||
}
|
||||
|
||||
/** Used to read variables from values. */
|
||||
public interface VariableFetcher
|
||||
{
|
||||
/** Reads the so-named variable from the supplied context object. */
|
||||
Object get (Object ctx, String name) throws Exception;
|
||||
}
|
||||
|
||||
/** Handles interpreting objects as collections. */
|
||||
public interface Collector
|
||||
{
|
||||
/** Returns an iterator that can iterate over the supplied value, or null if the value is
|
||||
* not a collection. */
|
||||
Iterator<?> toIterator (final Object value);
|
||||
|
||||
/** Creates a fetcher for a so-named variable on instances of the given class. */
|
||||
VariableFetcher createFetcher (Class<?> cclass, String name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a compiler that escapes HTML by default and does not use standards mode.
|
||||
*/
|
||||
public static Compiler compiler ()
|
||||
{
|
||||
return new Compiler(true, false, null, true, FAILING_LOADER);
|
||||
return new Compiler(true, false, null, true, FAILING_LOADER, new DefaultCollector());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -570,12 +597,10 @@ public class Mustache
|
||||
}
|
||||
@Override public void execute (Template tmpl, Template.Context ctx, Writer out) {
|
||||
Object value = tmpl.getSectionValue(ctx, _name, _line); // won't return null
|
||||
if (value instanceof Iterable<?>) {
|
||||
value = ((Iterable<?>)value).iterator();
|
||||
}
|
||||
if (value instanceof Iterator<?>) {
|
||||
Iterator<?> iter = tmpl._compiler.collector.toIterator(value);
|
||||
if (iter != null) {
|
||||
int index = 0;
|
||||
for (Iterator<?> iter = (Iterator<?>)value; iter.hasNext(); ) {
|
||||
while (iter.hasNext()) {
|
||||
Object elem = iter.next();
|
||||
boolean onFirst = (index == 0), onLast = !iter.hasNext();
|
||||
executeSegs(tmpl, ctx.nest(elem, ++index, onFirst, onLast), out);
|
||||
@@ -584,11 +609,6 @@ public class Mustache
|
||||
if ((Boolean)value) {
|
||||
executeSegs(tmpl, ctx, out);
|
||||
}
|
||||
} else if (value.getClass().isArray()) {
|
||||
for (int ii = 0, ll = Array.getLength(value); ii < ll; ii++) {
|
||||
boolean onFirst = (ii == 0), onLast = (ii == ll-1);
|
||||
executeSegs(tmpl, ctx.nest(Array.get(value, ii), ii+1, onFirst, onLast), out);
|
||||
}
|
||||
} else {
|
||||
executeSegs(tmpl, ctx.nest(value, 0, false, false), out);
|
||||
}
|
||||
@@ -602,24 +622,15 @@ public class Mustache
|
||||
}
|
||||
@Override public void execute (Template tmpl, Template.Context ctx, Writer out) {
|
||||
Object value = tmpl.getSectionValue(ctx, _name, _line); // won't return null
|
||||
if (value instanceof Iterable<?>) {
|
||||
Iterable<?> iable = (Iterable<?>)value;
|
||||
if (!iable.iterator().hasNext()) {
|
||||
Iterator<?> iter = tmpl._compiler.collector.toIterator(value);
|
||||
if (iter != null) {
|
||||
if (!iter.hasNext()) {
|
||||
executeSegs(tmpl, ctx, out);
|
||||
}
|
||||
} else if (value instanceof Boolean) {
|
||||
if (!(Boolean)value) {
|
||||
executeSegs(tmpl, ctx, out);
|
||||
}
|
||||
} else if (value.getClass().isArray()) {
|
||||
if (Array.getLength(value) == 0) {
|
||||
executeSegs(tmpl, ctx, out);
|
||||
}
|
||||
} else if (value instanceof Iterator<?>) {
|
||||
Iterator<?> iter = (Iterator<?>)value;
|
||||
if (!iter.hasNext()) {
|
||||
executeSegs(tmpl, ctx, out);
|
||||
}
|
||||
} // TODO: fail?
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,12 +7,10 @@ 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.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* Represents a compiled template. Templates are executed with a <em>context</em> to generate
|
||||
@@ -180,16 +178,19 @@ public class Template
|
||||
}
|
||||
|
||||
Key key = new Key(data.getClass(), name);
|
||||
VariableFetcher fetcher = _fcache.get(key);
|
||||
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 = createFetcher(key);
|
||||
fetcher = _compiler.collector.createFetcher(key.cclass, key.name);
|
||||
}
|
||||
} else {
|
||||
fetcher = createFetcher(key);
|
||||
fetcher = _compiler.collector.createFetcher(key.cclass, key.name);
|
||||
}
|
||||
|
||||
// if we were unable to create a fetcher, just return null and our caller can either try
|
||||
@@ -200,7 +201,9 @@ public class Template
|
||||
|
||||
try {
|
||||
Object value = fetcher.get(data, name);
|
||||
_fcache.put(key, fetcher);
|
||||
synchronized (_fcache) {
|
||||
_fcache.put(key, fetcher);
|
||||
}
|
||||
return value;
|
||||
} catch (Exception e) {
|
||||
throw new MustacheException(
|
||||
@@ -224,94 +227,8 @@ public class Template
|
||||
|
||||
protected final Segment[] _segs;
|
||||
protected final Mustache.Compiler _compiler;
|
||||
protected final Map<Key, VariableFetcher> _fcache =
|
||||
new ConcurrentHashMap<Key, VariableFetcher>();
|
||||
|
||||
protected static VariableFetcher createFetcher (Key key)
|
||||
{
|
||||
// support both .name and this.name to fetch members
|
||||
if (key.name == DOT_NAME || key.name == THIS_NAME) {
|
||||
return THIS_FETCHER;
|
||||
}
|
||||
|
||||
if (Map.class.isAssignableFrom(key.cclass)) {
|
||||
return MAP_FETCHER;
|
||||
}
|
||||
|
||||
final Method m = getMethod(key.cclass, key.name);
|
||||
if (m != null) {
|
||||
return new VariableFetcher() {
|
||||
@Override 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() {
|
||||
@Override public Object get (Object ctx, String name) throws Exception {
|
||||
return f.get(ctx);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected static Method getMethod (Class<?> clazz, String name)
|
||||
{
|
||||
Method m;
|
||||
try {
|
||||
m = clazz.getDeclaredMethod(name);
|
||||
if (!m.getReturnType().equals(void.class)) {
|
||||
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.getReturnType().equals(void.class)) {
|
||||
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;
|
||||
}
|
||||
protected final Map<Key, Mustache.VariableFetcher> _fcache =
|
||||
new HashMap<Key, Mustache.VariableFetcher>();
|
||||
|
||||
protected static class Context
|
||||
{
|
||||
@@ -369,22 +286,6 @@ public class Template
|
||||
}
|
||||
}
|
||||
|
||||
protected static abstract class VariableFetcher {
|
||||
abstract Object get (Object ctx, String name) throws Exception;
|
||||
}
|
||||
|
||||
protected static final VariableFetcher MAP_FETCHER = new VariableFetcher() {
|
||||
@Override public Object get (Object ctx, String name) throws Exception {
|
||||
return ((Map<?,?>)ctx).get(name);
|
||||
}
|
||||
};
|
||||
|
||||
protected static final VariableFetcher THIS_FETCHER = new VariableFetcher() {
|
||||
@Override public Object get (Object ctx, String name) throws Exception {
|
||||
return ctx;
|
||||
}
|
||||
};
|
||||
|
||||
protected static final Object NO_FETCHER_FOUND = new Object();
|
||||
|
||||
protected static final String DOT_NAME = ".".intern();
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// $Id$
|
||||
|
||||
package com.samskivert.mustache;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
* A collector used when running in GWT.
|
||||
*/
|
||||
public class DefaultCollector extends BasicCollector
|
||||
{
|
||||
@Override
|
||||
public Iterator<?> toIterator (final Object value) {
|
||||
Iterator<?> iter = super.toIterator(value);
|
||||
if (iter != null) return iter;
|
||||
|
||||
if (value.getClass().isArray()) {
|
||||
return Arrays.asList((Object[])value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,12 @@ public class MustacheTest
|
||||
});
|
||||
}
|
||||
|
||||
@Test public void testPrimitiveArrayVariable () {
|
||||
test("1234", "{{#foo}}{{this}}{{/foo}}", new Object() {
|
||||
int[] getFoo () { return new int[] { 1, 2, 3, 4 }; }
|
||||
});
|
||||
}
|
||||
|
||||
@Test public void testCallSiteReuse () {
|
||||
Template tmpl = Mustache.compiler().compile("{{foo}}");
|
||||
Object ctx = new Object() {
|
||||
|
||||
Reference in New Issue
Block a user