From 76e9d5468394fe0847029b7e303727c182a02609 Mon Sep 17 00:00:00 2001 From: Michael Bayne Date: Thu, 13 Oct 2011 20:35:46 -0700 Subject: [PATCH] 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. --- pom.xml | 3 + .../samskivert/mustache/BasicCollector.java | 51 ++++++++ .../samskivert/mustache/DefaultCollector.java | 116 +++++++++++++++++ .../com/samskivert/mustache/Mustache.java | 91 +++++++------ .../com/samskivert/mustache/Template.java | 123 ++---------------- .../samskivert/mustache/DefaultCollector.java | 24 ++++ .../com/samskivert/mustache/MustacheTest.java | 6 + 7 files changed, 263 insertions(+), 151 deletions(-) create mode 100644 src/main/java/com/samskivert/mustache/BasicCollector.java create mode 100644 src/main/java/com/samskivert/mustache/DefaultCollector.java create mode 100644 src/main/java/com/samskivert/mustache/gwt/com/samskivert/mustache/DefaultCollector.java 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() {