freemarker-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ddek...@apache.org
Subject [14/50] incubator-freemarker git commit: Added TemplateHashModelEx2 which allows key-value pair listing. Added <#list xs as k , v> and <#items as k, v>. More test will be added later.
Date Sun, 12 Jun 2016 16:53:55 GMT
Added TemplateHashModelEx2 which allows key-value pair listing. Added <#list xs as k ,v> and <#items as k, v>. More test will be added later.


Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/1ecf10a2
Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/1ecf10a2
Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/1ecf10a2

Branch: refs/heads/2.3
Commit: 1ecf10a286d0abd63c3162337f4f489900d24283
Parents: d616f29
Author: ddekany <ddekany@apache.org>
Authored: Sun May 29 11:24:05 2016 +0200
Committer: ddekany <ddekany@apache.org>
Committed: Mon May 30 00:11:04 2016 +0200

----------------------------------------------------------------------
 src/main/java/freemarker/core/Environment.java  |   8 +
 src/main/java/freemarker/core/Items.java        |  43 +++-
 .../java/freemarker/core/IteratorBlock.java     | 225 +++++++++++++++----
 .../core/NonSequenceOrCollectionException.java  |   8 +-
 .../freemarker/ext/beans/SimpleMapModel.java    |   8 +-
 .../freemarker/template/DefaultMapAdapter.java  |   6 +-
 .../template/MapKeyValuePairIterator.java       |  69 ++++++
 .../java/freemarker/template/SimpleHash.java    |  13 +-
 .../template/TemplateHashModelEx2.java          |  43 ++++
 src/main/javacc/FTL.jj                          |  64 +++++-
 src/manual/en_US/book.xml                       | 125 +++++++++--
 .../freemarker/core/ListValidationsTest.java    |  14 ++
 12 files changed, 546 insertions(+), 80 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/1ecf10a2/src/main/java/freemarker/core/Environment.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/Environment.java b/src/main/java/freemarker/core/Environment.java
index 20e910c..8d03ae5 100644
--- a/src/main/java/freemarker/core/Environment.java
+++ b/src/main/java/freemarker/core/Environment.java
@@ -60,6 +60,7 @@ import freemarker.template.TemplateException;
 import freemarker.template.TemplateExceptionHandler;
 import freemarker.template.TemplateHashModel;
 import freemarker.template.TemplateHashModelEx;
+import freemarker.template.TemplateHashModelEx2.KeyValuePairIterator;
 import freemarker.template.TemplateModel;
 import freemarker.template.TemplateModelException;
 import freemarker.template.TemplateModelIterator;
@@ -2992,6 +2993,13 @@ public final class Environment extends Configurable {
             ensureInitializedRTE();
             return super.values();
         }
+
+        @Override
+        public KeyValuePairIterator keyValuePairIterator() {
+            ensureInitializedRTE();
+            return super.keyValuePairIterator();
+        }
+
         
     }
 

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/1ecf10a2/src/main/java/freemarker/core/Items.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/Items.java b/src/main/java/freemarker/core/Items.java
index f588093..a56192b 100644
--- a/src/main/java/freemarker/core/Items.java
+++ b/src/main/java/freemarker/core/Items.java
@@ -29,9 +29,16 @@ import freemarker.template.TemplateException;
 class Items extends TemplateElement {
 
     private final String loopVarName;
+    private final String loopVar2Name;
 
-    Items(String loopVariableName, TemplateElements children) {
-        this.loopVarName = loopVariableName;
+    /**
+     * @param loopVar2Name
+     *            For non-hash listings always {@code null}, for hash listings {@code loopVarName} and
+     *            {@code loopVarName2} holds the key- and value loop variable names.
+     */
+    Items(String loopVarName, String loopVar2Name, TemplateElements children) {
+        this.loopVarName = loopVarName;
+        this.loopVar2Name = loopVar2Name;
         setChildren(children);
     }
 
@@ -44,7 +51,7 @@ class Items extends TemplateElement {
                     getNodeTypeSymbol(), " without iteration in context");
         }
         
-        iterCtx.loopForItemsElement(env, getChildBuffer(), loopVarName);
+        iterCtx.loopForItemsElement(env, getChildBuffer(), loopVarName, loopVar2Name);
         return null;
     }
 
@@ -59,7 +66,11 @@ class Items extends TemplateElement {
         if (canonical) sb.append('<');
         sb.append(getNodeTypeSymbol());
         sb.append(" as ");
-        sb.append(loopVarName);
+        sb.append(_CoreStringUtils.toFTLTopLevelIdentifierReference(loopVarName));
+        if (loopVar2Name != null) {
+            sb.append(", ");
+            sb.append(_CoreStringUtils.toFTLTopLevelIdentifierReference(loopVar2Name));
+        }
         if (canonical) {
             sb.append('>');
             sb.append(getChildrenCanonicalForm());
@@ -77,19 +88,33 @@ class Items extends TemplateElement {
 
     @Override
     int getParameterCount() {
-        return 1;
+        return loopVar2Name != null ? 2 : 1;
     }
 
     @Override
     Object getParameterValue(int idx) {
-        return loopVarName;
+        switch (idx) {
+        case 0:
+            if (loopVarName == null) throw new IndexOutOfBoundsException();
+            return loopVarName;
+        case 1:
+            if (loopVar2Name == null) throw new IndexOutOfBoundsException();
+            return loopVar2Name;
+        default: throw new IndexOutOfBoundsException();
+        }
     }
 
     @Override
     ParameterRole getParameterRole(int idx) {
-        if (idx == 0) return ParameterRole.TARGET_LOOP_VARIABLE;
-        else
-            throw new IndexOutOfBoundsException();
+        switch (idx) {
+        case 0:
+            if (loopVarName == null) throw new IndexOutOfBoundsException();
+            return ParameterRole.TARGET_LOOP_VARIABLE;
+        case 1:
+            if (loopVar2Name == null) throw new IndexOutOfBoundsException();
+            return ParameterRole.TARGET_LOOP_VARIABLE;
+        default: throw new IndexOutOfBoundsException();
+        }
     }
 
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/1ecf10a2/src/main/java/freemarker/core/IteratorBlock.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/IteratorBlock.java b/src/main/java/freemarker/core/IteratorBlock.java
index 2a4df44..4161c92 100644
--- a/src/main/java/freemarker/core/IteratorBlock.java
+++ b/src/main/java/freemarker/core/IteratorBlock.java
@@ -28,9 +28,15 @@ import freemarker.template.SimpleNumber;
 import freemarker.template.TemplateBooleanModel;
 import freemarker.template.TemplateCollectionModel;
 import freemarker.template.TemplateException;
+import freemarker.template.TemplateHashModel;
+import freemarker.template.TemplateHashModelEx;
+import freemarker.template.TemplateHashModelEx2;
+import freemarker.template.TemplateHashModelEx2.KeyValuePair;
+import freemarker.template.TemplateHashModelEx2.KeyValuePairIterator;
 import freemarker.template.TemplateModel;
 import freemarker.template.TemplateModelException;
 import freemarker.template.TemplateModelIterator;
+import freemarker.template.TemplateScalarModel;
 import freemarker.template.TemplateSequenceModel;
 import freemarker.template.utility.Constants;
 
@@ -39,27 +45,48 @@ import freemarker.template.utility.Constants;
  */
 final class IteratorBlock extends TemplateElement {
 
-    private final Expression listExp;
+    private final Expression listedExp;
     private final String loopVarName;
-    private final boolean isForEach;
+    private final String loopVar2Name;
+    private final boolean hashListing;
+    private final boolean forEach;
 
     /**
-     * @param listExp
-     *            a variable referring to a sequence or collection ("the list" from now on)
+     * @param listedExp
+     *            a variable referring to a sequence or collection or extended hash to list
      * @param loopVarName
-     *            The name of the variable that will hold the value of the current item when looping through the list.
+     *            The name of the variable that will hold the value of the current item when looping through listed value,
+     *            or {@code null} if we have a nested {@code #items}. If this is a hash listing then this variable will holds the value
+     *            of the hash key.
+     * @param loopVar2Name
+     *            The name of the variable that will hold the value of the current item when looping through the list,
+     *            or {@code null} if we have a nested {@code #items}. If this is a hash listing then it variable will hold the value
+     *            from the key-value pair.
      * @param childrenBeforeElse
-     *            The nested content to execute if the list wasn't empty; can't be {@code null}. If the loop variable
-     *            was specified in the start tag, this is also what we will iterator over.
+     *            The nested content to execute if the listed value wasn't empty; can't be {@code null}. If the loop variable
+     *            was specified in the start tag, this is also what we will iterate over.
+     * @param hashListing
+     *            Whether this is a key-value pair listing, or a usual listing. This is properly set even if we have
+     *            a nested {@code #items}.
+     * @param forEach
+     *            Whether this is {@code #foreach} or a {@code #list}.
      */
-    IteratorBlock(Expression listExp,
+    IteratorBlock(Expression listedExp,
                   String loopVarName,
+                  String loopVar2Name,
                   TemplateElements childrenBeforeElse,
-                  boolean isForEach) {
-        this.listExp = listExp;
+                  boolean hashListing,
+                  boolean forEach) {
+        this.listedExp = listedExp;
         this.loopVarName = loopVarName;
+        this.loopVar2Name = loopVar2Name;
         setChildren(childrenBeforeElse);
-        this.isForEach = isForEach;
+        this.hashListing = hashListing;
+        this.forEach = forEach;
+    }
+    
+    boolean isHashListing() {
+        return hashListing;
     }
 
     @Override
@@ -69,16 +96,16 @@ final class IteratorBlock extends TemplateElement {
     }
     
     boolean acceptWithResult(Environment env) throws TemplateException, IOException {
-        TemplateModel listValue = listExp.eval(env);
-        if (listValue == null) {
+        TemplateModel listedValue = listedExp.eval(env);
+        if (listedValue == null) {
             if (env.isClassicCompatible()) {
-                listValue = Constants.EMPTY_SEQUENCE; 
+                listedValue = Constants.EMPTY_SEQUENCE; 
             } else {
-                listExp.assertNonNull(null, env);
+                listedExp.assertNonNull(null, env);
             }
         }
 
-        return env.visitIteratorBlock(new IterationContext(listValue, loopVarName));
+        return env.visitIteratorBlock(new IterationContext(listedValue, loopVarName, loopVar2Name));
     }
 
     /**
@@ -95,7 +122,9 @@ final class IteratorBlock extends TemplateElement {
                 Object ctx = ctxStack.get(i);
                 if (ctx instanceof IterationContext
                         && (loopVariableName == null
-                            || loopVariableName.equals(((IterationContext) ctx).getLoopVariableName()))) {
+                            || loopVariableName.equals(((IterationContext) ctx).getLoopVariableName())
+                            || loopVariableName.equals(((IterationContext) ctx).getLoopVariable2Name())
+                            )) {
                     return (IterationContext) ctx;
                 }
             }
@@ -109,15 +138,19 @@ final class IteratorBlock extends TemplateElement {
         if (canonical) buf.append('<');
         buf.append(getNodeTypeSymbol());
         buf.append(' ');
-        if (isForEach) {
+        if (forEach) {
             buf.append(_CoreStringUtils.toFTLTopLevelIdentifierReference(loopVarName));
             buf.append(" in ");
-            buf.append(listExp.getCanonicalForm());
+            buf.append(listedExp.getCanonicalForm());
         } else {
-            buf.append(listExp.getCanonicalForm());
+            buf.append(listedExp.getCanonicalForm());
             if (loopVarName != null) {
                 buf.append(" as ");
                 buf.append(_CoreStringUtils.toFTLTopLevelIdentifierReference(loopVarName));
+                if (loopVar2Name != null) {
+                    buf.append(", ");
+                    buf.append(_CoreStringUtils.toFTLTopLevelIdentifierReference(loopVar2Name));
+                }
             }
         }
         if (canonical) {
@@ -134,17 +167,20 @@ final class IteratorBlock extends TemplateElement {
     
     @Override
     int getParameterCount() {
-        return loopVarName != null ? 2 : 1;
+        return 1 + (loopVarName != null ? 1 : 0) + (loopVar2Name != null ? 1 : 0);
     }
 
     @Override
     Object getParameterValue(int idx) {
         switch (idx) {
         case 0:
-            return listExp;
+            return listedExp;
         case 1:
             if (loopVarName == null) throw new IndexOutOfBoundsException();
             return loopVarName;
+        case 2:
+            if (loopVar2Name == null) throw new IndexOutOfBoundsException();
+            return loopVar2Name;
         default: throw new IndexOutOfBoundsException();
         }
     }
@@ -157,13 +193,16 @@ final class IteratorBlock extends TemplateElement {
         case 1:
             if (loopVarName == null) throw new IndexOutOfBoundsException();
             return ParameterRole.TARGET_LOOP_VARIABLE;
+        case 2:
+            if (loopVar2Name == null) throw new IndexOutOfBoundsException();
+            return ParameterRole.TARGET_LOOP_VARIABLE;
         default: throw new IndexOutOfBoundsException();
         }
     }    
     
     @Override
     String getNodeTypeSymbol() {
-        return isForEach ? "#foreach" : "#list";
+        return forEach ? "#foreach" : "#list";
     }
 
     @Override
@@ -182,25 +221,29 @@ final class IteratorBlock extends TemplateElement {
         private TemplateModelIterator openedIteratorModel;
         private boolean hasNext;
         private TemplateModel loopVar;
+        private TemplateModel loopVar2;
         private int index;
         private boolean alreadyEntered;
         private Collection localVarNames = null;
         
         /** If the {@code #list} has nested {@code #items}, it's {@code null} outside the {@code #items}. */
         private String loopVarName;
+        /** Used if we list key-value pairs */
+        private String loopVar2Name;
         
-        private final TemplateModel listValue;
+        private final TemplateModel listedValue;
         
-        public IterationContext(TemplateModel listValue, String loopVariableName) {
-            this.listValue = listValue;
-            this.loopVarName = loopVariableName;
+        public IterationContext(TemplateModel listedValue, String loopVarName, String loopVar2Name) {
+            this.listedValue = listedValue;
+            this.loopVarName = loopVarName;
+            this.loopVar2Name = loopVar2Name;
         }
         
         boolean accept(Environment env) throws TemplateException, IOException {
             return executeNestedContent(env, getChildBuffer());
         }
 
-        void loopForItemsElement(Environment env, TemplateElement[] childBuffer, String loopVarName)
+        void loopForItemsElement(Environment env, TemplateElement[] childBuffer, String loopVarName, String loopVar2Name)
                     throws NonSequenceOrCollectionException, TemplateModelException, InvalidReferenceException,
                     TemplateException, IOException {
             try {
@@ -210,35 +253,119 @@ final class IteratorBlock extends TemplateElement {
                 }
                 alreadyEntered = true;
                 this.loopVarName = loopVarName;
+                this.loopVar2Name = loopVar2Name;
                 executeNestedContent(env, childBuffer);
             } finally {
                 this.loopVarName = null;
+                this.loopVar2Name = null;
             }
         }
 
         /**
-         * Executes the given block for the {@link #listValue}: if {@link #loopVarName} is non-{@code null}, then for
-         * each list item once, otherwise once if {@link #listValue} isn't empty.
+         * Executes the given block for the {@link #listedValue}: if {@link #loopVarName} is non-{@code null}, then for
+         * each list item once, otherwise once if {@link #listedValue} isn't empty.
          */
         private boolean executeNestedContent(Environment env, TemplateElement[] childBuffer)
                 throws TemplateModelException, TemplateException, IOException, NonSequenceOrCollectionException,
                 InvalidReferenceException {
+            return !hashListing
+                    ? executedNestedContentForNonHashListing(env, childBuffer)
+                    : executedNestedContentForHashListing(env, childBuffer);
+        }
+
+        private boolean executedNestedContentForHashListing(Environment env, TemplateElement[] childBuffer)
+                throws TemplateModelException, IOException, TemplateException {
+            final boolean hashNotEmpty;
+            if (listedValue instanceof TemplateHashModelEx) {
+                TemplateHashModelEx listedHash = (TemplateHashModelEx) listedValue; 
+                if (listedHash instanceof TemplateHashModelEx2) {
+                    KeyValuePairIterator kvpIter = ((TemplateHashModelEx2) listedHash).keyValuePairIterator();
+                    hashNotEmpty = kvpIter.hasNext();
+                    if (hashNotEmpty) {
+                        if (loopVarName != null) {
+                            try {
+                                do {
+                                    KeyValuePair kvp = kvpIter.next();
+                                    loopVar = kvp.getKey();
+                                    loopVar2 = kvp.getValue();
+                                    hasNext = kvpIter.hasNext();
+                                    env.visit(childBuffer);
+                                    index++;
+                                } while (hasNext);
+                            } catch (BreakInstruction.Break br) {
+                                // Silently exit loop
+                            }
+                        } else {
+                            env.visit(childBuffer);
+                        }
+                    }
+                } else { //  not a TemplateHashModelEx2, but still a TemplateHashModelEx
+                    TemplateModelIterator keysIter = listedHash.keys().iterator();
+                    hashNotEmpty = keysIter.hasNext();
+                    if (hashNotEmpty) {
+                        if (loopVarName != null) {
+                            try {
+                                do {
+                                    loopVar = keysIter.next();
+                                    if (!(loopVar instanceof TemplateScalarModel)) {
+                                        throw new NonStringException(env,
+                                                new _ErrorDescriptionBuilder(
+                                                        "When listing key-value pairs of traditional hash "
+                                                        + "implementations, all keys must be strings, but one of them "
+                                                        + "was ",
+                                                        new _DelayedAOrAn(new _DelayedFTLTypeDescription(loopVar)), "."
+                                                        ).tip("The listed value's TemplateModel class was ",
+                                                                new _DelayedShortClassName(listedValue.getClass()),
+                                                                ", which doesn't implement ",
+                                                                new _DelayedShortClassName(TemplateHashModelEx2.class),
+                                                                ", which leads to this restriction."));
+                                    }
+                                    loopVar2 = listedHash.get(((TemplateScalarModel) loopVar).getAsString());
+                                    hasNext = keysIter.hasNext();
+                                    env.visit(childBuffer);
+                                    index++;
+                                } while (hasNext);
+                            } catch (BreakInstruction.Break br) {
+                                // Silently exit loop
+                            }
+                        } else {
+                            env.visit(childBuffer);
+                        }
+                    }
+                }
+            } else if (listedValue instanceof TemplateCollectionModel
+                    || listedValue instanceof TemplateSequenceModel) {
+                throw new NonSequenceOrCollectionException(env,
+                        new _ErrorDescriptionBuilder("The value you try to list is ",
+                                new _DelayedAOrAn(new _DelayedFTLTypeDescription(listedValue)),
+                                ", thus you must specify only one loop variable after the \"as\" (there's no separate "
+                                + "key and value)."
+                                ));
+            } else {
+                throw new NonExtendedHashException(
+                        listedExp, listedValue, env);
+            }
+            return hashNotEmpty;
+        }
+
+        private boolean executedNestedContentForNonHashListing(Environment env, TemplateElement[] childBuffer)
+                throws TemplateModelException, IOException, TemplateException,
+                NonSequenceOrCollectionException, InvalidReferenceException {
             final boolean listNotEmpty;
-            if (listValue instanceof TemplateCollectionModel) {
-                final TemplateCollectionModel collModel = (TemplateCollectionModel) listValue;
+            if (listedValue instanceof TemplateCollectionModel) {
+                final TemplateCollectionModel collModel = (TemplateCollectionModel) listedValue;
                 final TemplateModelIterator iterModel
                         = openedIteratorModel == null ? collModel.iterator() : openedIteratorModel;
-                hasNext = iterModel.hasNext();
-                listNotEmpty = hasNext;
+                listNotEmpty = iterModel.hasNext();
                 if (listNotEmpty) {
                     if (loopVarName != null) {
                         try {
-                            while (hasNext) {
+                            do {
                                 loopVar = iterModel.next();
                                 hasNext = iterModel.hasNext();
                                 env.visit(childBuffer);
                                 index++;
-                            }
+                            } while (hasNext);
                         } catch (BreakInstruction.Break br) {
                             // Silently exit loop
                         }
@@ -250,8 +377,8 @@ final class IteratorBlock extends TemplateElement {
                         env.visit(childBuffer);
                     }
                 }
-            } else if (listValue instanceof TemplateSequenceModel) {
-                final TemplateSequenceModel seqModel = (TemplateSequenceModel) listValue;
+            } else if (listedValue instanceof TemplateSequenceModel) {
+                final TemplateSequenceModel seqModel = (TemplateSequenceModel) listedValue;
                 final int size = seqModel.size();
                 listNotEmpty = size != 0;
                 if (listNotEmpty) {
@@ -272,7 +399,7 @@ final class IteratorBlock extends TemplateElement {
             } else if (env.isClassicCompatible()) {
                 listNotEmpty = true;
                 if (loopVarName != null) {
-                    loopVar = listValue;
+                    loopVar = listedValue;
                     hasNext = false;
                 }
                 try {
@@ -280,11 +407,18 @@ final class IteratorBlock extends TemplateElement {
                 } catch (BreakInstruction.Break br) {
                     // Silently exit "loop"
                 }
+            } else if (listedValue instanceof TemplateHashModelEx
+                    && !NonSequenceOrCollectionException.isWrappedIterable(listedValue)) {
+                throw new NonSequenceOrCollectionException(env,
+                        new _ErrorDescriptionBuilder("The value you try to list is ",
+                                new _DelayedAOrAn(new _DelayedFTLTypeDescription(listedValue)),
+                                ", thus you must specify two loop variables after the \"as\"; one for the key, and "
+                                + "another for the value, like ", "<#... as k, v>", ")."
+                                ));
             } else {
                 throw new NonSequenceOrCollectionException(
-                        listExp, listValue, env);
+                        listedExp, listedValue, env);
             }
-            
             return listNotEmpty;
         }
 
@@ -292,6 +426,10 @@ final class IteratorBlock extends TemplateElement {
             return this.loopVarName;
         }
 
+        String getLoopVariable2Name() {
+            return this.loopVar2Name;
+        }
+        
         public TemplateModel getLocalVariable(String name) {
             String loopVariableName = this.loopVarName;
             if (loopVariableName != null && name.startsWith(loopVariableName)) {
@@ -310,6 +448,11 @@ final class IteratorBlock extends TemplateElement {
                         break;
                 }
             }
+            
+            if (name.equals(loopVar2Name)) {
+                return loopVar2;
+            }
+            
             return null;
         }
         

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/1ecf10a2/src/main/java/freemarker/core/NonSequenceOrCollectionException.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/NonSequenceOrCollectionException.java b/src/main/java/freemarker/core/NonSequenceOrCollectionException.java
index bc172f4..2487b61 100644
--- a/src/main/java/freemarker/core/NonSequenceOrCollectionException.java
+++ b/src/main/java/freemarker/core/NonSequenceOrCollectionException.java
@@ -73,8 +73,7 @@ public class NonSequenceOrCollectionException extends UnexpectedTypeException {
     }
     
     private static Object[] extendTipsIfIterable(TemplateModel model, Object[] tips) {
-        if (model instanceof WrapperTemplateModel
-                && ((WrapperTemplateModel) model).getWrappedObject() instanceof Iterable) {
+        if (isWrappedIterable(model)) {
             final int tipsLen = tips != null ? tips.length : 0;
             Object[] extendedTips = new Object[tipsLen + 1];
             for (int i = 0; i < tipsLen; i++) {
@@ -87,4 +86,9 @@ public class NonSequenceOrCollectionException extends UnexpectedTypeException {
         }
     }
 
+    public static boolean isWrappedIterable(TemplateModel model) {
+        return model instanceof WrapperTemplateModel
+                && ((WrapperTemplateModel) model).getWrappedObject() instanceof Iterable;
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/1ecf10a2/src/main/java/freemarker/ext/beans/SimpleMapModel.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/ext/beans/SimpleMapModel.java b/src/main/java/freemarker/ext/beans/SimpleMapModel.java
index f5f3eac..24a2540 100644
--- a/src/main/java/freemarker/ext/beans/SimpleMapModel.java
+++ b/src/main/java/freemarker/ext/beans/SimpleMapModel.java
@@ -26,10 +26,12 @@ import freemarker.core.CollectionAndSequence;
 import freemarker.ext.util.ModelFactory;
 import freemarker.ext.util.WrapperTemplateModel;
 import freemarker.template.AdapterTemplateModel;
+import freemarker.template.MapKeyValuePairIterator;
 import freemarker.template.ObjectWrapper;
 import freemarker.template.SimpleSequence;
 import freemarker.template.TemplateCollectionModel;
 import freemarker.template.TemplateHashModelEx;
+import freemarker.template.TemplateHashModelEx2;
 import freemarker.template.TemplateMethodModelEx;
 import freemarker.template.TemplateModel;
 import freemarker.template.TemplateModelException;
@@ -44,7 +46,7 @@ import freemarker.template.utility.RichObjectWrapper;
  * and a method interface to non-string keys.
  */
 public class SimpleMapModel extends WrappingTemplateModel 
-implements TemplateHashModelEx, TemplateMethodModelEx, AdapterTemplateModel, 
+implements TemplateHashModelEx2, TemplateMethodModelEx, AdapterTemplateModel, 
 WrapperTemplateModel, TemplateModelWithAPISupport {
     static final ModelFactory FACTORY =
         new ModelFactory()
@@ -103,6 +105,10 @@ WrapperTemplateModel, TemplateModelWithAPISupport {
         return new CollectionAndSequence(new SimpleSequence(map.values(), getObjectWrapper()));
     }
     
+    public KeyValuePairIterator keyValuePairIterator() {
+        return new MapKeyValuePairIterator(map, getObjectWrapper());
+    }
+
     public Object getAdaptedObject(Class hint) {
         return map;
     }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/1ecf10a2/src/main/java/freemarker/template/DefaultMapAdapter.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/template/DefaultMapAdapter.java b/src/main/java/freemarker/template/DefaultMapAdapter.java
index 56f9443..68c1438 100644
--- a/src/main/java/freemarker/template/DefaultMapAdapter.java
+++ b/src/main/java/freemarker/template/DefaultMapAdapter.java
@@ -45,7 +45,7 @@ import freemarker.template.utility.ObjectWrapperWithAPISupport;
  * @since 2.3.22
  */
 public class DefaultMapAdapter extends WrappingTemplateModel
-        implements TemplateHashModelEx, AdapterTemplateModel, WrapperTemplateModel, TemplateModelWithAPISupport,
+        implements TemplateHashModelEx2, AdapterTemplateModel, WrapperTemplateModel, TemplateModelWithAPISupport,
         Serializable {
 
     private final Map map;
@@ -134,6 +134,10 @@ public class DefaultMapAdapter extends WrappingTemplateModel
         return new SimpleCollection(map.values(), getObjectWrapper());
     }
 
+    public KeyValuePairIterator keyValuePairIterator() {
+        return new MapKeyValuePairIterator(map, getObjectWrapper());
+    }
+
     public Object getAdaptedObject(Class hint) {
         return map;
     }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/1ecf10a2/src/main/java/freemarker/template/MapKeyValuePairIterator.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/template/MapKeyValuePairIterator.java b/src/main/java/freemarker/template/MapKeyValuePairIterator.java
new file mode 100644
index 0000000..4c5c1c0
--- /dev/null
+++ b/src/main/java/freemarker/template/MapKeyValuePairIterator.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package freemarker.template;
+
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import freemarker.template.TemplateHashModelEx2.KeyValuePair;
+import freemarker.template.TemplateHashModelEx2.KeyValuePairIterator;
+
+/**
+ *  Implementation of {@link KeyValuePairIterator} for a {@link TemplateHashModelEx2} that wraps or otherwise uses a
+ *  {@link Map} internally.
+ *
+ *  @since 2.3.25
+ */
+public class MapKeyValuePairIterator implements KeyValuePairIterator {
+
+    private final Iterator<Entry<?, ?>> entrySetIterator;
+    
+    private final ObjectWrapper objectWrapper;
+    
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    public <K, V> MapKeyValuePairIterator(Map<?, ?> map, ObjectWrapper objectWrapper) {
+        entrySetIterator = ((Map) map).entrySet().iterator();
+        this.objectWrapper = objectWrapper;
+    }
+
+    public boolean hasNext() {
+        return entrySetIterator.hasNext();
+    }
+
+    public KeyValuePair next() {
+        final Entry<?, ?> entry = entrySetIterator.next();
+        return new KeyValuePair() {
+
+            public TemplateModel getKey() throws TemplateModelException {
+                return wrap(entry.getKey());
+            }
+
+            public TemplateModel getValue() throws TemplateModelException {
+                return wrap(entry.getValue());
+            }
+            
+        };
+    }
+    
+    private TemplateModel wrap(Object obj) throws TemplateModelException {
+        return (obj instanceof TemplateModel) ? (TemplateModel) obj : objectWrapper.wrap(obj);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/1ecf10a2/src/main/java/freemarker/template/SimpleHash.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/template/SimpleHash.java b/src/main/java/freemarker/template/SimpleHash.java
index 6ffb9af..4b1bf0f 100644
--- a/src/main/java/freemarker/template/SimpleHash.java
+++ b/src/main/java/freemarker/template/SimpleHash.java
@@ -71,7 +71,7 @@ import freemarker.ext.beans.BeansWrapper;
  * @see DefaultMapAdapter
  * @see TemplateHashModelEx
  */
-public class SimpleHash extends WrappingTemplateModel implements TemplateHashModelEx, Serializable {
+public class SimpleHash extends WrappingTemplateModel implements TemplateHashModelEx2, Serializable {
 
     private final Map map;
     private boolean putFailed;
@@ -341,6 +341,10 @@ public class SimpleHash extends WrappingTemplateModel implements TemplateHashMod
         return new SimpleCollection(map.values(), getObjectWrapper());
     }
 
+    public KeyValuePairIterator keyValuePairIterator() {
+        return new MapKeyValuePairIterator(map, getObjectWrapper());
+    }
+
     public SimpleHash synchronizedWrapper() {
         return new SynchronizedHash();
     }
@@ -395,6 +399,13 @@ public class SimpleHash extends WrappingTemplateModel implements TemplateHashMod
                 return SimpleHash.this.values();
             }
         }
+
+        @Override
+        public KeyValuePairIterator keyValuePairIterator() {
+            synchronized (SimpleHash.this) {
+                return SimpleHash.this.keyValuePairIterator();
+            }
+        }
         
         @Override
         public Map toMap() throws TemplateModelException {

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/1ecf10a2/src/main/java/freemarker/template/TemplateHashModelEx2.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/template/TemplateHashModelEx2.java b/src/main/java/freemarker/template/TemplateHashModelEx2.java
new file mode 100644
index 0000000..9e7a8d7
--- /dev/null
+++ b/src/main/java/freemarker/template/TemplateHashModelEx2.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package freemarker.template;
+
+/**
+ * Adds key-value pair listing capability to {@link TemplateHashModelEx}. While in many cases that can also be achieved
+ * with {@link #keys()} and then {@link #get(String)}, that has some problems. One is that {@link #get(String)} only
+ * accepts string keys, while {@link #keys()} can return non-string keys too. The other is that {@link #keys()} and then
+ * {@link #get(String)} for each key can be slower than listing the key-value pairs in one go.
+ * 
+ * @since 2.3.25 
+ */
+public interface TemplateHashModelEx2 extends TemplateHashModelEx {
+
+    KeyValuePairIterator keyValuePairIterator();
+    
+    interface KeyValuePair {
+        TemplateModel getKey() throws TemplateModelException;
+        TemplateModel getValue() throws TemplateModelException;
+    }
+    
+    interface KeyValuePairIterator {
+        boolean hasNext() throws TemplateModelException;
+        KeyValuePair next() throws TemplateModelException;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/1ecf10a2/src/main/javacc/FTL.jj
----------------------------------------------------------------------
diff --git a/src/main/javacc/FTL.jj b/src/main/javacc/FTL.jj
index d934298..46e565a 100644
--- a/src/main/javacc/FTL.jj
+++ b/src/main/javacc/FTL.jj
@@ -45,8 +45,27 @@ public class FMParser {
     private static final int ITERATOR_BLOCK_KIND_USER_DIRECTIVE = 3; 
 
     private static class ParserIteratorBlockContext {
+        /**
+         * loopVarName in <#list ... as loopVarName> or <#items as loopVarName>; null after we left the nested
+         * block of #list or #items, respectively.
+         */
         private String loopVarName;
+        
+        /**
+         * loopVar1Name in <#list ... as k, loopVar2Name> or <#items as k, loopVar2Name>; null after we left the nested
+         * block of #list or #items, respectively.
+         */
+        private String loopVar2Name;
+        
+        /**
+         * See the ITERATOR_BLOCK_KIND_... costants.
+         */
         private int kind;
+        
+        /**
+         * Is this a key-value pair listing? When there's a nested #items, it's only set there. 
+         */
+        private boolean hashListing;
     }
 
     private Template template;
@@ -507,7 +526,7 @@ public class FMParser {
         int size = iteratorBlockContexts != null ? iteratorBlockContexts.size() : 0;
         for (int i = size - 1; i >= 0; i--) {
             ParserIteratorBlockContext ctx = (ParserIteratorBlockContext) iteratorBlockContexts.get(i);
-            if (loopVarName.equals(ctx.loopVarName)) {
+            if (loopVarName.equals(ctx.loopVarName) || loopVarName.equals(ctx.loopVar2Name)) {
                 if (ctx.kind == ITERATOR_BLOCK_KIND_USER_DIRECTIVE) {
 			        throw new ParseException(
 			                "The left hand operand of ?" + biName.image
@@ -2530,7 +2549,7 @@ RecoveryBlock Recover() :
 TemplateElement List() :
 {
     Expression exp;
-    Token loopVar = null, start, end;
+    Token loopVar = null, loopVar2 = null, start, end;
     TemplateElements childrendBeforeElse;
     ElseOfList elseOfList = null;
     ParserIteratorBlockContext iterCtx;
@@ -2541,6 +2560,10 @@ TemplateElement List() :
     [
         <AS>
         loopVar = <ID>
+        [
+            <COMMA>
+            loopVar2 = <ID>
+        ]
     ]
     <DIRECTIVE_END>
     {
@@ -2548,6 +2571,15 @@ TemplateElement List() :
         if (loopVar != null) {
             iterCtx.loopVarName = loopVar.image;
             breakableDirectiveNesting++;
+            if (loopVar2 != null) {
+                iterCtx.loopVar2Name = loopVar2.image;
+                iterCtx.hashListing = true;
+                if (iterCtx.loopVar2Name.equals(iterCtx.loopVarName)) {
+                    throw new ParseException(
+                            "The key and value loop variable names must differ, but both were: " + iterCtx.loopVarName,
+                            template, start);
+                }
+            }
         }
     }
     
@@ -2569,7 +2601,11 @@ TemplateElement List() :
     
     end = <END_LIST>
     {
-        IteratorBlock list = new IteratorBlock(exp, loopVar != null ? loopVar.image : null, childrendBeforeElse, false);
+        IteratorBlock list = new IteratorBlock(
+                exp,
+                loopVar != null ? loopVar.image : null,  // null when we have a nested #items
+                loopVar2 != null ? loopVar2.image : null,
+                childrendBeforeElse, iterCtx.hashListing, false);
         list.setLocation(template, start, end);
 
         TemplateElement result;
@@ -2624,7 +2660,7 @@ IteratorBlock ForEach() :
         breakableDirectiveNesting--;
         popIteratorBlockContext();
                 
-        IteratorBlock result = new IteratorBlock(exp, loopVar.image, children, true);
+        IteratorBlock result = new IteratorBlock(exp, loopVar.image, null, children, false, true);
         result.setLocation(template, start, end);
         return result;
     }
@@ -2632,13 +2668,17 @@ IteratorBlock ForEach() :
 
 Items Items() :
 {
-    Token loopVar, start, end;
+    Token loopVar, loopVar2 = null, start, end;
     TemplateElements children;
     ParserIteratorBlockContext iterCtx;
 }
 {
     start = <ITEMS>
     loopVar = <ID>
+    [
+        <COMMA>
+        loopVar2 = <ID>
+    ]
     <DIRECTIVE_END>
     {
         iterCtx = peekIteratorBlockContext();
@@ -2650,7 +2690,7 @@ Items Items() :
 	        if (iterCtx.kind == ITERATOR_BLOCK_KIND_FOREACH) {
 	            msg = forEachDirectiveSymbol() + " doesn't support nested #items.";
 	        } else if (iterCtx.kind == ITERATOR_BLOCK_KIND_ITEMS) {
-                msg = "Can't nest #items into each other that belong to the same #list.";
+                msg = "Can't nest #items into each other when they belong to the same #list.";
 	        } else {
 	            msg = "The parent #list of the #items must not have \"as loopVar\" parameter.";
             }
@@ -2658,6 +2698,15 @@ Items Items() :
         }
         iterCtx.kind = ITERATOR_BLOCK_KIND_ITEMS;
         iterCtx.loopVarName = loopVar.image;
+        if (loopVar2 != null) {
+            iterCtx.loopVar2Name = loopVar2.image;
+            iterCtx.hashListing = true;
+            if (iterCtx.loopVar2Name.equals(iterCtx.loopVarName)) {
+                throw new ParseException(
+                        "The key and value loop variable names must differ, but both were: " + iterCtx.loopVarName,
+                        template, start);
+            }
+        }
     
         breakableDirectiveNesting++;
     }
@@ -2668,8 +2717,9 @@ Items Items() :
     {
         breakableDirectiveNesting--;
         iterCtx.loopVarName = null;
+        iterCtx.loopVar2Name = null;
         
-        Items result = new Items(loopVar.image, children);
+        Items result = new Items(loopVar.image, loopVar2 != null ? loopVar2.image : null, children);
         result.setLocation(template, start, end);
         return result;
     }

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/1ecf10a2/src/manual/en_US/book.xml
----------------------------------------------------------------------
diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml
index 2e2c83e..516ed4b 100644
--- a/src/manual/en_US/book.xml
+++ b/src/manual/en_US/book.xml
@@ -16795,17 +16795,19 @@ Sorted by name.last:
             <primary>keys built-in</primary>
           </indexterm>
 
-          <para>A sequence that contains all the lookup keys in the hash. Note
-          that not all hashes support this (ask the programmer if a certain
-          hash allows this or not).</para>
+          <para>A sequence that contains all the lookup keys in the
+          hash.</para>
 
-          <programlisting role="template">&lt;#assign h = {"name":"mouse", "price":50}&gt;
-&lt;#assign keys = h?keys&gt;
-&lt;#list keys as key&gt;${key} = ${h[key]}; &lt;/#list&gt;</programlisting>
+          <programlisting role="template">&lt;#assign myHash = { "name": "mouse", "price": 50 }&gt;
+&lt;#list myHash?keys as k&gt;
+  ${k}
+&lt;/#list&gt;</programlisting>
 
-          <para>Output:</para>
+          <programlisting role="output">  name
+  price</programlisting>
 
-          <programlisting role="output">name = mouse; price = 50;</programlisting>
+          <para>Note that not all hashes support this (ask the programmer if a
+          certain hash allows this or not).</para>
 
           <para>Since hashes do not define an order for their sub variables in
           general, the order in which key names are returned can be arbitrary.
@@ -16814,6 +16816,14 @@ Sorted by name.last:
           with the above <literal>{<replaceable>...</replaceable>}</literal>
           syntax preserve the same order as you have specified the sub
           variables.</para>
+
+          <note>
+            <para>To list both the keys and the values, you can use
+            <literal>&lt;#list attrs as key,
+            value&gt;...&lt;#list&gt;</literal>; see the <link
+            linkend="ref.directive.list"><literal>list</literal>
+            directive</link>.</para>
+          </note>
         </section>
 
         <section xml:id="ref_builtin_values">
@@ -16823,13 +16833,33 @@ Sorted by name.last:
             <primary>values built-in</primary>
           </indexterm>
 
-          <para>A sequence that contains all the variables in the hash. Note
-          that not all hashes support this (ask the programmer if a certain
-          hash allows this or not).</para>
+          <para>A sequence that contains all the variables (the values in the
+          key-value pairs) in the hash.</para>
+
+          <programlisting role="template">&lt;#assign myHash = { "name": "mouse", "price": 50 }&gt;
+&lt;#list myHash?values as v&gt;
+  ${v}
+&lt;/#list&gt;</programlisting>
+
+          <programlisting role="output">  mouse
+  50</programlisting>
+
+          <para>Note that not all hashes support this (ask the programmer if a
+          certain hash allows this or not).</para>
 
           <para>As of the order in which the values are returned, the same
-          applies as with the <literal>keys</literal> built-in; see
-          there.</para>
+          applies as with the <literal>keys</literal> built-in; see there.
+          Furthermore, it's not guaranteed that the order of the values
+          corresponds to the order of the keys returned by the
+          <literal>keys</literal> build-in.</para>
+
+          <note>
+            <para>To list both the keys and the values, you can use
+            <literal>&lt;#list attrs as key,
+            value&gt;...&lt;#list&gt;</literal>; see the <link
+            linkend="ref.directive.list"><literal>list</literal>
+            directive</link>.</para>
+          </note>
         </section>
       </section>
 
@@ -20159,7 +20189,29 @@ All rights reserved.</emphasis></programlisting>
         <section>
           <title>Synopsis</title>
 
-          <para>Form 1:</para>
+          <para>The simplest form for listing a sequence (or collection)
+          is:</para>
+
+          <programlisting role="metaTemplate"><literal>&lt;#list <replaceable>sequence</replaceable> as <replaceable>item</replaceable>&gt;
+    <replaceable>Part repeated for each item</replaceable>
+&lt;/#list&gt;</literal></programlisting>
+
+          <para>and to list the key-value pairs of a hash (since
+          2.3.25):</para>
+
+          <programlisting role="metaTemplate"><literal>&lt;#list <replaceable>hash</replaceable> as <replaceable>key</replaceable>, <replaceable>value</replaceable>&gt;
+    <replaceable>Part repeated for each key-value pair</replaceable>
+&lt;/#list&gt;</literal></programlisting>
+
+          <para>But these are just cases of the generic forms, which are shown
+          below. Note that for simplicity we only show the generic forms for
+          sequence listing; simply replace <quote><literal>as
+          <replaceable>item</replaceable></literal></quote> with
+          <quote><literal>as <replaceable>key</replaceable>,
+          <replaceable>value</replaceable></literal></quote> to get the
+          generic form for hash listing.</para>
+
+          <para>Generic form 1:</para>
 
           <programlisting role="metaTemplate"><literal>&lt;#list <replaceable>sequence</replaceable> as <replaceable>item</replaceable>&gt;
     <replaceable>Part repeated for each item</replaceable>
@@ -20194,7 +20246,7 @@ All rights reserved.</emphasis></programlisting>
             </listitem>
           </itemizedlist>
 
-          <para>Form 2 (since FreeMarker 2.3.23):</para>
+          <para>Generic form 2 (since FreeMarker 2.3.23):</para>
 
           <programlisting role="metaTemplate"><literal>&lt;#list <replaceable>sequence</replaceable>&gt;
     <replaceable>Part executed once if we have more than 0 items</replaceable>
@@ -20206,8 +20258,9 @@ All rights reserved.</emphasis></programlisting>
     <replaceable>Part executed when there are 0 items</replaceable>
 &lt;/#list&gt;</literal></programlisting>
 
-          <para>Where: Same as the <quote>Where</quote> section of Form 1
-          above.</para>
+          <para>Where: see the <quote>Where</quote> section of Form 1 above
+          (and thus the <literal>else</literal> part is optional here
+          too).</para>
         </section>
 
         <section>
@@ -20239,6 +20292,25 @@ All rights reserved.</emphasis></programlisting>
             inside the <literal>list</literal> body. Also, macros/functions
             called from within the loop won't see it (as if it were a local
             variable).</para>
+
+            <para>Listing hashes is very similar, but you need to provide two
+            variable names after the <literal>as</literal>; one for the hash
+            key, and another for the associated value. Assuming
+            <literal>products</literal> is <literal>{ "apple": 5, "banana":
+            10, "kiwi": 15 }</literal>:</para>
+
+            <programlisting role="template">&lt;#list products as name, price&gt;
+  &lt;p&gt;${name}: ${price}
+&lt;/#list&gt;</programlisting>
+
+            <programlisting role="output">  &lt;p&gt;apple: 5
+  &lt;p&gt;banan: 10
+  &lt;p&gt;kiwi: 15</programlisting>
+
+            <para>Note that not all hash variables can be listed, because some
+            of them isn't able to enumerate its keys. It's practically safe to
+            assume though that hashes that stand for Java
+            <literal>Map</literal> objects can be listed.</para>
           </section>
 
           <section>
@@ -26497,7 +26569,14 @@ TemplateModel x = env.getVariable("x");  // get variable x</programlisting>
 
           <itemizedlist>
             <listitem>
-              <para>[TODO]</para>
+              <para>Added the <literal>TemplateModelHashEx2</literal>
+              interface which extends <literal>TemplateModelHashEx</literal>
+              with a method for listing the content of the key-value pairs of
+              the hash. (This is utilized by the new hash listing capability
+              of the <link
+              linkend="ref.directive.list"><literal>list</literal>
+              directive</link>, but it's not required by it if all keys are
+              strings.)</para>
             </listitem>
           </itemizedlist>
         </section>
@@ -26507,6 +26586,16 @@ TemplateModel x = env.getVariable("x");  // get variable x</programlisting>
 
           <itemizedlist>
             <listitem>
+              <para>Extended the <link
+              linkend="ref.directive.list"><literal>list</literal>
+              directive</link> to support listing hashes (such as
+              <literal>Map</literal>-s), like <literal>&lt;#list map as k,
+              v&gt;${k}: ${v}&lt;/#list&gt;</literal>, where
+              <literal>k</literal> and <literal>v</literal> is key-value
+              pair.</para>
+            </listitem>
+
+            <listitem>
               <para>Lazy imports: With the new boolean
               <literal>Configuration</literal>-level settings,
               <literal>lazy_imports</literal> and

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/1ecf10a2/src/test/java/freemarker/core/ListValidationsTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/freemarker/core/ListValidationsTest.java b/src/test/java/freemarker/core/ListValidationsTest.java
index 892e0c0..eed74ce 100644
--- a/src/test/java/freemarker/core/ListValidationsTest.java
+++ b/src/test/java/freemarker/core/ListValidationsTest.java
@@ -107,5 +107,19 @@ public class ListValidationsTest extends TemplateTest {
                 + "</@></#list>",
                 "?index", "foo" , "user defined directive");
     }
+
+    @Test
+    public void testKeyValueSameName() {
+        assertErrorContains("<#list {} as foo, foo></#list>",
+                "key", "value", "both" , "foo");
+    }
+
+    @Test
+    public void testCollectionVersusHash() {
+        assertErrorContains("<#list {} as i></#list>",
+                "as k, v");
+        assertErrorContains("<#list [] as k, v></#list>",
+                "only one loop variable");
+    }
     
 }


Mime
View raw message