diff --git a/pom.xml b/pom.xml index 1340403..029713f 100644 --- a/pom.xml +++ b/pom.xml @@ -62,6 +62,9 @@ true -Xlint" "-Xlint:-serial" "-Xlint:-path + + com/samskivert/mustache/gwt/** + diff --git a/src/main/java/com/samskivert/mustache/BasicCollector.java b/src/main/java/com/samskivert/mustache/BasicCollector.java new file mode 100644 index 0000000..278ea35 --- /dev/null +++ b/src/main/java/com/samskivert/mustache/BasicCollector.java @@ -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; + } + }; +} diff --git a/src/main/java/com/samskivert/mustache/DefaultCollector.java b/src/main/java/com/samskivert/mustache/DefaultCollector.java new file mode 100644 index 0000000..078de3a --- /dev/null +++ b/src/main/java/com/samskivert/mustache/DefaultCollector.java @@ -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() { + 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; + } +} diff --git a/src/main/java/com/samskivert/mustache/Mustache.java b/src/main/java/com/samskivert/mustache/Mustache.java index 812da94..a9467ff 100644 --- a/src/main/java/com/samskivert/mustache/Mustache.java +++ b/src/main/java/com/samskivert/mustache/Mustache.java @@ -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 Mustache templating services. - * Basic usage: {@code + * + * Basic usage: + * {@code * String source = "Hello {{arg}}!"; * Template tmpl = Mustache.compiler().compile(source); * Map context = new HashMap(); * context.put("arg", "world"); - * tmpl.execute(context); // returns "Hello world!" } + * tmpl.execute(context); // returns "Hello world!" + * } + * * Limitations: - * Only one or two character delimiters are supported when using {{=ab cd=}} to change + * + * Only one or two character delimiters are supported when using {{=ab cd=}} to change * delimiters. * {{< include}} is not supported. We specifically do not want the complexity of handling the - * automatic loading of dependent templates. + * automatic loading of dependent templates. + * */ 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? } } diff --git a/src/main/java/com/samskivert/mustache/Template.java b/src/main/java/com/samskivert/mustache/Template.java index 7995250..bc31afb 100644 --- a/src/main/java/com/samskivert/mustache/Template.java +++ b/src/main/java/com/samskivert/mustache/Template.java @@ -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 context 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 _fcache = - new ConcurrentHashMap(); - - 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 _fcache = + new HashMap(); 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(); diff --git a/src/main/java/com/samskivert/mustache/gwt/com/samskivert/mustache/DefaultCollector.java b/src/main/java/com/samskivert/mustache/gwt/com/samskivert/mustache/DefaultCollector.java new file mode 100644 index 0000000..474974f --- /dev/null +++ b/src/main/java/com/samskivert/mustache/gwt/com/samskivert/mustache/DefaultCollector.java @@ -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; + } +} diff --git a/src/test/java/com/samskivert/mustache/MustacheTest.java b/src/test/java/com/samskivert/mustache/MustacheTest.java index d46a5c7..3a55f33 100644 --- a/src/test/java/com/samskivert/mustache/MustacheTest.java +++ b/src/test/java/com/samskivert/mustache/MustacheTest.java @@ -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() {
Basic usage:
{@code + * + * Basic usage: + * {@code * String source = "Hello {{arg}}!"; * Template tmpl = Mustache.compiler().compile(source); * Map context = new HashMap(); * context.put("arg", "world"); - * tmpl.execute(context); // returns "Hello world!" } + * tmpl.execute(context); // returns "Hello world!" + * }
Basic usage: + *
{@code * String source = "Hello {{arg}}!"; * Template tmpl = Mustache.compiler().compile(source); * Map context = new HashMap(); * context.put("arg", "world"); - * tmpl.execute(context); // returns "Hello world!" }
Limitations: - *