Repository: incubator-freemarker Updated Branches: refs/heads/2.3-gae d616f2936 -> 1ecf10a28
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-gae Commit: 1ecf10a286d0abd63c3162337f4f489900d24283 Parents: d616f29 Author: ddekany <[email protected]> Authored: Sun May 29 11:24:05 2016 +0200 Committer: ddekany <[email protected]> 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"); + } }
