You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2016/06/12 16:53:55 UTC
[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.
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 <dd...@apache.org>
Authored: Sun May 29 11:24:05 2016 +0200
Committer: ddekany <dd...@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"><#assign h = {"name":"mouse", "price":50}>
-<#assign keys = h?keys>
-<#list keys as key>${key} = ${h[key]}; </#list></programlisting>
+ <programlisting role="template"><#assign myHash = { "name": "mouse", "price": 50 }>
+<#list myHash?keys as k>
+ ${k}
+</#list></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><#list attrs as key,
+ value>...<#list></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"><#assign myHash = { "name": "mouse", "price": 50 }>
+<#list myHash?values as v>
+ ${v}
+</#list></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><#list attrs as key,
+ value>...<#list></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><#list <replaceable>sequence</replaceable> as <replaceable>item</replaceable>>
+ <replaceable>Part repeated for each item</replaceable>
+</#list></literal></programlisting>
+
+ <para>and to list the key-value pairs of a hash (since
+ 2.3.25):</para>
+
+ <programlisting role="metaTemplate"><literal><#list <replaceable>hash</replaceable> as <replaceable>key</replaceable>, <replaceable>value</replaceable>>
+ <replaceable>Part repeated for each key-value pair</replaceable>
+</#list></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><#list <replaceable>sequence</replaceable> as <replaceable>item</replaceable>>
<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><#list <replaceable>sequence</replaceable>>
<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>
</#list></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"><#list products as name, price>
+ <p>${name}: ${price}
+</#list></programlisting>
+
+ <programlisting role="output"> <p>apple: 5
+ <p>banan: 10
+ <p>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><#list map as k,
+ v>${k}: ${v}</#list></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");
+ }
}