diff --git a/src/main/java/com/x5/template/Chunk.java b/src/main/java/com/x5/template/Chunk.java index 0ea1aa4..0ffa098 100644 --- a/src/main/java/com/x5/template/Chunk.java +++ b/src/main/java/com/x5/template/Chunk.java @@ -36,7 +36,7 @@ * set up replacement rules for those tags like so: * *
- *    TemplateSet templates = getTemplates(); // defined elsewhere
+ *    Theme templates = getTemplates(); // defined elsewhere
  *    Chunk myChunk = templates.makeChunk("my_template");
  *    myChunk.set("my_tag","hello tag");
  *    System.out.print( myChunk.toString() );
@@ -48,9 +48,8 @@
  * hash mark surrounded by curly brackets/braces.
  *
  * 

- * TemplateSet is handy if you have a folder with lots of html templates.
* Here's an even simpler example, where the template string is supplied
- * without using TemplateSet: + * without using a theme: * *

  *    String templateBody = "Hello {$name}!  Your balance is ${$balance}."
@@ -91,9 +90,9 @@
  * 

* A: No*. To keep things simple and reduce potential for confusion, Chunk
* does not auto-magically fill any tags based on naming conventions or
- * in-template directives. You must explicitly invoke the "include command:
+ * in-template directives. You must explicitly invoke the include command:
* - * bla bla bla {.include #myTemplate} foo foo foo + * bla bla bla {% include #myTemplate %} foo foo foo * *

* Actually, this documentation is outdated, and several extensions to the
* original template syntax are now available: @@ -111,7 +110,7 @@ * Q: My final output says "infinite recursion detected." What gives? * *

- * A: You did some variation of this: + * A: You tripped the recursion depth limit (17) or you did some variation of this: *

  *   TEMPLATE:
  *     bla bla bla {$name}
@@ -167,10 +166,9 @@
  * Q: Are tag names and subtemplate names case sensitive?
  *
  * 

- * A: Yes. I prefer to use mixed case in {$tagNames} with first letter
- * lowercase. In my experience this aids readability since tags are similar
- * to java variables in concept and that is the java case convention for
- * variables. Similarly, I prefer lowercase with underscores for all
+ * A: Yes. I prefer to use snake case in {$tag_names} but be aware
+ * that {$tag} and {$Tag} and {$TAG} are three different values.
+ * Similarly, I prefer lowercase with underscores for all
* {#sub_template_names}{#} since templates tend to be defined within html
* files which are typically named in all lowercase. * @@ -245,7 +243,7 @@ * Updates: Chunk Documentation
* * @author Tom McClure - * @version 3.0.0 + * @version 3.0.1 */ public class Chunk implements Map @@ -253,7 +251,7 @@ public class Chunk implements Map public static final int HASH_THRESH = 8; public static final int DEPTH_LIMIT = 17; - public static final String VERSION = "3.0.0"; + public static final String VERSION = "3.0.1"; private static final String TRUE = "TRUE"; @@ -461,18 +459,7 @@ public void set(String tagName, Object tagValue, String ifNull) if (tagName == null) return; // ensure that tagValue is either a String or a Chunk (or some tabular data) if (tagValue != null) { - tagValue = coercePrimitivesToString(tagValue); - if (tagValue instanceof Chunk || tagValue instanceof TableData) { - // don't treat chunk or tabledata as a Map - } else if (tagValue instanceof Map) { - // great, a map! - } else if (!(tagValue instanceof String - || tagValue instanceof Snippet - || tagValue instanceof List - || tagValue instanceof Object[])) { - // force to map - tagValue = new ObjectDataMap(tagValue); - } + tagValue = coercePrimitivesToStringAndBoxAliens(tagValue); } if (tagValue == null) { tagValue = (ifNull == null) ? "NULL" : ifNull; @@ -1086,9 +1073,9 @@ protected Object _resolveTagValue(SnippetTag tag, int depth, boolean ignoreParen tagValue = null; } } - // convert primitives to string - if (!(tagValue instanceof String)) { - tagValue = coercePrimitivesToString(tagValue); + // convert primitives to string, box illegal aliens + if (tagValue != null && !(tagValue instanceof String)) { + tagValue = coercePrimitivesToStringAndBoxAliens(tagValue); } String filters = tag.getFilters(); @@ -1126,16 +1113,40 @@ protected Object _resolveTagValue(SnippetTag tag, int depth, boolean ignoreParen } } - // unbox and stringify primitive wrapper objects - private Object coercePrimitivesToString(Object o) + // unbox and stringify primitive wrapper objects, box any objects if not chunk-friendly + private Object coercePrimitivesToStringAndBoxAliens(Object o) { + if (o == null) return o; + if (o instanceof Boolean) { return ((Boolean)o).booleanValue() ? "TRUE" : null; } else if (o != null && ObjectDataMap.isWrapperType(o.getClass())) { return o.toString(); } else { + return boxIfAlienObject(o); + } + } + + private Object boxIfAlienObject(Object o) + { + if (o == null) return o; + + if (o instanceof Chunk || o instanceof TableData) { + // Chunk and TableData can be handled natively + return o; + } else if (o instanceof Map) { + // Map can be handled natively + return o; + } else if (o instanceof String + || o instanceof Snippet + || o instanceof List + || o instanceof Object[]) { + // can all be handled natively return o; } + + // unrecognized object. wrap inside map. + return new ObjectDataMap(o); } protected Object resolveTagValue(String tagName, int depth) @@ -1288,7 +1299,7 @@ public void setMultiple(Map rules) if (rules == null || rules.size() <= 0) return; Set keys = rules.keySet(); for (String tagName : keys) { - setOrDelete(tagName,rules.get(tagName)); + setOrDelete(tagName, rules.get(tagName)); } } diff --git a/src/main/java/com/x5/template/LoopTag.java b/src/main/java/com/x5/template/LoopTag.java index db311c8..5c4b97d 100644 --- a/src/main/java/com/x5/template/LoopTag.java +++ b/src/main/java/com/x5/template/LoopTag.java @@ -241,7 +241,7 @@ private TableData fetchData(String dataVar, String origin) data = new TableOfMaps(list); } else { // last-ditch effort to extract data, treat as POJOs - data = TableOfMaps.boxObjectList((List)dataStore); + data = TableOfMaps.boxCollection(list); } } } else if (dataStore instanceof Object[]) { @@ -252,8 +252,21 @@ private TableData fetchData(String dataVar, String origin) data = TableOfMaps.boxObjectArray((Object[])dataStore); } } else if (dataStore instanceof Map) { - Map object = (Map)dataStore; - data = new ObjectTable(object); + if (dataStore instanceof com.x5.util.ObjectDataMap) { + Object unwrapped = ((com.x5.util.ObjectDataMap)dataStore).unwrap(); + if (unwrapped instanceof java.util.Collection) { + data = TableOfMaps.boxCollection((java.util.Collection)unwrapped); + } else if (unwrapped instanceof java.util.Enumeration) { + data = TableOfMaps.boxEnumeration((java.util.Enumeration)unwrapped); + } else if (unwrapped instanceof java.util.Iterator) { + data = TableOfMaps.boxIterator((java.util.Iterator)unwrapped); + } + } + if (data == null) { + // Doesn't support traditional iteration. Loop over object's keys:values instead. + Map object = (Map)dataStore; + data = new ObjectTable(object); + } } // only loop if following pointer diff --git a/src/main/java/com/x5/template/TableOfMaps.java b/src/main/java/com/x5/template/TableOfMaps.java index 4bbe28f..e19e501 100644 --- a/src/main/java/com/x5/template/TableOfMaps.java +++ b/src/main/java/com/x5/template/TableOfMaps.java @@ -1,6 +1,8 @@ package com.x5.template; import java.util.ArrayList; +import java.util.Collection; +import java.util.Enumeration; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -79,13 +81,46 @@ static TableData boxObjectList(List dataStore) return null; } + return boxIterator(dataStore.iterator()); + } + + @SuppressWarnings("rawtypes") + static TableData boxEnumeration(Enumeration dataStore) + { + if (dataStore == null || !dataStore.hasMoreElements()) { + return null; + } + + // convert to list of POJOs + List boxedObjects = new ArrayList(); + while (dataStore.hasMoreElements()) { + boxedObjects.add(new ObjectDataMap(dataStore.nextElement())); + } + + return new TableOfMaps(boxedObjects); + } + + @SuppressWarnings("rawtypes") + static TableData boxCollection(Collection collection) + { + if (collection == null || collection.size() < 1) { + return null; + } + + return boxIterator(collection.iterator()); + } + + static TableData boxIterator(Iterator i) + { + if (i == null || !i.hasNext()) { + return null; + } + List boxedObjects = new ArrayList(); - Iterator i = dataStore.iterator(); while (i.hasNext()) { boxedObjects.add(new ObjectDataMap(i.next())); } return new TableOfMaps(boxedObjects); } - } diff --git a/src/main/java/com/x5/template/filters/DefaultFilter.java b/src/main/java/com/x5/template/filters/DefaultFilter.java index 09b61eb..5984e9f 100644 --- a/src/main/java/com/x5/template/filters/DefaultFilter.java +++ b/src/main/java/com/x5/template/filters/DefaultFilter.java @@ -24,4 +24,9 @@ public String getFilterName() return "default"; } + public String[] getFilterAliases() + { + return new String[]{"onnull"}; + } + } diff --git a/src/main/java/com/x5/template/filters/OnEmptyFilter.java b/src/main/java/com/x5/template/filters/OnEmptyFilter.java index 338df4d..a89073b 100644 --- a/src/main/java/com/x5/template/filters/OnEmptyFilter.java +++ b/src/main/java/com/x5/template/filters/OnEmptyFilter.java @@ -15,7 +15,7 @@ public String transformText(Chunk chunk, String text, FilterArgs arg) if (args != null && args.length > 0) swapFor = args[0]; if (swapFor == null) return null; - // null and empty string are both considered empty + // null and empty string and whitespace-only are all considered empty return (text == null || text.trim().length() == 0) ? FilterArgs.magicBraces(chunk, swapFor) : text; } diff --git a/src/test/java/com/x5/template/LoopTest.java b/src/test/java/com/x5/template/LoopTest.java index 0ab8b89..ad0effd 100644 --- a/src/test/java/com/x5/template/LoopTest.java +++ b/src/test/java/com/x5/template/LoopTest.java @@ -1,5 +1,8 @@ package com.x5.template; +import java.util.Map; +import java.util.StringTokenizer; + import org.junit.Test; import static org.junit.Assert.*; @@ -363,11 +366,37 @@ public void testLoopOverSplit() { Theme theme = new Theme(); Chunk c = theme.makeChunk(); - c.set("nums","1,2,3,4"); + c.set("nums", "1,2,3,4"); c.append("{.loop in $nums|split(,) as $n}{$n}{.divider} {/divider}{/loop}"); assertEquals("1 2 3 4", c.toString()); } + @Test + public void testLoopOverNestedEnumeration() + { + Theme theme = new Theme(); + Chunk c = theme.makeChunk(); + StringTokenizer tokens = new StringTokenizer("a b c d"); + Map map = new java.util.HashMap(); + map.put("tokens", tokens); + c.set("token_holder", map); + c.append("{% loop in $token_holder.tokens as $token divider='-' %}{$token}{% endloop %}"); + + assertEquals("a-b-c-d", c.toString()); + } + + @Test + public void testLoopOverEnumeration() + { + Theme theme = new Theme(); + Chunk c = theme.makeChunk(); + StringTokenizer tokens = new StringTokenizer("a b c d"); + c.set("tokens", tokens); + c.append("{% loop in $tokens as $token divider='-' %}{$token}{% endloop %}"); + + assertEquals("a-b-c-d", c.toString()); + } + @Test public void testBadCloseTag() {