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 2019/06/23 23:25:53 UTC
[freemarker] branch 2.3-gae updated: Now operations that require a
sequence will only accept a lazily generated collection (typically returned
by ?filter or ?map) if it was explicitly marked as a sequence (via
LazilyGeneratedCollectionModel.isSequence()). Thus,
applying operations like ?filter or ?map on a non-sequence enumerable value
won't allow sequence-only operations. This was made this strict as if a
huge collection was passed to the template as Iterator,
an implicit conversion to sequence can consume too much memory. [...]
This is an automated email from the ASF dual-hosted git repository.
ddekany pushed a commit to branch 2.3-gae
in repository https://gitbox.apache.org/repos/asf/freemarker.git
The following commit(s) were added to refs/heads/2.3-gae by this push:
new e05bd9a Now operations that require a sequence will only accept a lazily generated collection (typically returned by ?filter or ?map) if it was explicitly marked as a sequence (via LazilyGeneratedCollectionModel.isSequence()). Thus, applying operations like ?filter or ?map on a non-sequence enumerable value won't allow sequence-only operations. This was made this strict as if a huge collection was passed to the template as Iterator, an implicit conversion to sequence can consum [...]
e05bd9a is described below
commit e05bd9a699eadce4a0b50de950b5da8ae0cf0f11
Author: ddekany <dd...@apache.org>
AuthorDate: Sun Jun 23 16:55:25 2019 +0200
Now operations that require a sequence will only accept a lazily generated collection (typically returned by ?filter or ?map) if it was explicitly marked as a sequence (via LazilyGeneratedCollectionModel.isSequence()). Thus, applying operations like ?filter or ?map on a non-sequence enumerable value won't allow sequence-only operations. This was made this strict as if a huge collection was passed to the template as Iterator, an implicit conversion to sequence can consume too much mem [...]
---
src/main/java/freemarker/core/BuiltIn.java | 7 +-
.../freemarker/core/BuiltInsForMultipleTypes.java | 7 +-
.../java/freemarker/core/BuiltInsForSequences.java | 118 ++++++++++++++-------
src/main/java/freemarker/core/DynamicKeyName.java | 19 ++--
.../core/LazilyGeneratedCollectionModel.java | 80 ++++++++++++++
....java => LazilyGeneratedCollectionModelEx.java} | 16 ++-
...eratedCollectionModelWithAlreadyKnownSize.java} | 12 ++-
...eneratedCollectionModelWithSameSizeCollEx.java} | 16 +--
...lyGeneratedCollectionModelWithSameSizeSeq.java} | 17 +--
...lyGeneratedCollectionModelWithUnknownSize.java} | 20 ++--
.../core/SingleIterationCollectionModel.java | 4 +
src/main/java/freemarker/core/_CoreAPI.java | 5 +-
src/main/java/freemarker/core/_MessageUtil.java | 22 ++--
.../freemarker/template/utility/ClassUtil.java | 5 +-
src/manual/en_US/book.xml | 26 +++++
src/test/java/freemarker/core/FilterBiTest.java | 25 +++++
...est.java => LazilyGeneratedCollectionTest.java} | 94 ++++++++++------
src/test/java/freemarker/core/MapBiTest.java | 19 ++++
18 files changed, 382 insertions(+), 130 deletions(-)
diff --git a/src/main/java/freemarker/core/BuiltIn.java b/src/main/java/freemarker/core/BuiltIn.java
index 57c3313..4e26f50 100644
--- a/src/main/java/freemarker/core/BuiltIn.java
+++ b/src/main/java/freemarker/core/BuiltIn.java
@@ -393,7 +393,12 @@ abstract class BuiltIn extends Expression implements Cloneable {
return bi;
}
- /** If the built-in supports a lazily generated value as its left operand (the target). */
+ /**
+ * If the built-in supports a lazily generated value as its left operand (the target).
+ * Don't confuse this with what's allowed for result of the built-in itself; that's influenced by
+ * {@link Expression#enableLazilyGeneratedResult()} (and so
+ * {@link BuiltInsForSequences.IntermediateStreamOperationLikeBuiltIn#isLazilyGeneratedTargetResultSupported()}).
+ */
protected boolean isLazilyGeneratedTargetResultSupported() {
return false;
}
diff --git a/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java b/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
index c8a0922..34ff1f0 100644
--- a/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
+++ b/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
@@ -504,14 +504,15 @@ class BuiltInsForMultipleTypes {
size = ((TemplateCollectionModelEx) model).size();
} else if (model instanceof TemplateHashModelEx) {
size = ((TemplateHashModelEx) model).size();
- } else if (model instanceof LazilyGeneratedSequenceModel) {
+ } else if (model instanceof LazilyGeneratedCollectionModel
+ && ((LazilyGeneratedCollectionModel) model).isSequence()) {
// While this is a TemplateCollectionModel, and thus ?size will be O(N), and N might be infinite,
// it's for the result of ?filter(predicate) or such. Those "officially" return a sequence. Returning a
- // TemplateCollectionModel (a LazilyGeneratedSequenceModel more specifically) is a (mostly) transparent
+ // TemplateCollectionModel (a LazilyGeneratedCollectionModel to be exact) is a (mostly) transparent
// optimization to avoid creating the result sequence in memory, which would be unnecessary work for
// ?size. Creating that result sequence would be O(N) too, so the O(N) time complexity should be
// expected by the template author, and we just made that calculation less wasteful here.
- TemplateModelIterator iterator = ((LazilyGeneratedSequenceModel) model).iterator();
+ TemplateModelIterator iterator = ((LazilyGeneratedCollectionModel) model).iterator();
int counter = 0;
countElements: while (iterator.hasNext()) {
counter++;
diff --git a/src/main/java/freemarker/core/BuiltInsForSequences.java b/src/main/java/freemarker/core/BuiltInsForSequences.java
index ba40615..a232025 100644
--- a/src/main/java/freemarker/core/BuiltInsForSequences.java
+++ b/src/main/java/freemarker/core/BuiltInsForSequences.java
@@ -855,6 +855,8 @@ class BuiltInsForSequences {
static class sequenceBI extends BuiltIn {
+ private boolean lazilyGeneratedResultEnabled;
+
@Override
TemplateModel _eval(Environment env) throws TemplateException {
TemplateModel model = target.eval(env);
@@ -867,17 +869,37 @@ class BuiltInsForSequences {
throw new NonSequenceOrCollectionException(target, model, env);
}
TemplateCollectionModel coll = (TemplateCollectionModel) model;
-
- SimpleSequence seq =
- coll instanceof TemplateCollectionModelEx
- ? new SimpleSequence(((TemplateCollectionModelEx) coll).size())
- : new SimpleSequence();
- for (TemplateModelIterator iter = coll.iterator(); iter.hasNext(); ) {
- seq.add(iter.next());
+
+ if (!lazilyGeneratedResultEnabled) {
+ SimpleSequence seq =
+ coll instanceof TemplateCollectionModelEx
+ ? new SimpleSequence(((TemplateCollectionModelEx) coll).size())
+ : new SimpleSequence();
+ for (TemplateModelIterator iter = coll.iterator(); iter.hasNext(); ) {
+ seq.add(iter.next());
+ }
+ return seq;
+ } else {
+ return coll instanceof LazilyGeneratedCollectionModel
+ ? ((LazilyGeneratedCollectionModel) coll).withIsSequenceTrue()
+ : coll instanceof TemplateCollectionModelEx
+ ? new LazilyGeneratedCollectionModelWithSameSizeCollEx(
+ new LazyCollectionTemplateModelIterator(coll),
+ (TemplateCollectionModelEx) coll, true)
+ : new LazilyGeneratedCollectionModelWithUnknownSize(
+ new LazyCollectionTemplateModelIterator(coll), true);
}
- return seq;
}
-
+
+ @Override
+ void enableLazilyGeneratedResult() {
+ lazilyGeneratedResultEnabled = true;
+ }
+
+ @Override
+ protected boolean isLazilyGeneratedTargetResultSupported() {
+ return true;
+ }
}
private static boolean isBuggySeqButGoodCollection(
@@ -1001,7 +1023,8 @@ class BuiltInsForSequences {
if (elementTransformerExp instanceof LocalLambdaExpression) {
LocalLambdaExpression localLambdaExp = (LocalLambdaExpression) elementTransformerExp;
checkLocalLambdaParamCount(localLambdaExp, 1);
- // We can't do this with other kind of expressions, as they need to be evaluated on runtime:
+ // We can't do this with other kind of expressions, like a function or method reference, as they
+ // need to be evaluated on runtime:
precreatedElementTransformer = new LocalLambdaElementTransformer(localLambdaExp);
}
}
@@ -1053,9 +1076,32 @@ class BuiltInsForSequences {
}
TemplateModel _eval(Environment env) throws TemplateException {
- TemplateModel lho = target.eval(env);
- TemplateModelIterator lhoIterator = getTemplateModelIterator(env, lho);
- return calculateResult(lhoIterator, lho, evalElementTransformerExp(env), env);
+ TemplateModel targetValue = target.eval(env);
+
+ final TemplateModelIterator targetIterator;
+ final boolean targetIsSequence;
+ {
+ if (targetValue instanceof TemplateCollectionModel) {
+ targetIterator = isLazilyGeneratedResultEnabled()
+ ? new LazyCollectionTemplateModelIterator((TemplateCollectionModel) targetValue)
+ : ((TemplateCollectionModel) targetValue).iterator();
+ targetIsSequence = targetValue instanceof LazilyGeneratedCollectionModel ?
+ ((LazilyGeneratedCollectionModel) targetValue).isSequence() : false;
+ } else if (targetValue instanceof TemplateSequenceModel) {
+ targetIterator = new LazySequenceIterator((TemplateSequenceModel) targetValue);
+ targetIsSequence = true;
+ } else if (targetValue instanceof TemplateModelIterator) {
+ targetIterator = (TemplateModelIterator) targetValue;
+ targetIsSequence = false;
+ } else {
+ throw new NonSequenceOrCollectionException(target, targetValue, env);
+ }
+ }
+
+ return calculateResult(
+ targetIterator, targetValue, targetIsSequence,
+ evalElementTransformerExp(env),
+ env);
}
private ElementTransformer evalElementTransformerExp(Environment env) throws TemplateException {
@@ -1073,31 +1119,18 @@ class BuiltInsForSequences {
}
}
- private TemplateModelIterator getTemplateModelIterator(Environment env, TemplateModel model) throws TemplateModelException,
- NonSequenceOrCollectionException, InvalidReferenceException {
- if (model instanceof TemplateCollectionModel) {
- return isLazilyGeneratedResultEnabled()
- ? new LazyCollectionTemplateModelIterator((TemplateCollectionModel) model)
- : ((TemplateCollectionModel) model).iterator();
- } else if (model instanceof TemplateSequenceModel) {
- return new LazySequenceIterator((TemplateSequenceModel) model);
- } else if (model instanceof TemplateModelIterator) { // For a lazily generated LHO
- return (TemplateModelIterator) model;
- } else {
- throw new NonSequenceOrCollectionException(target, model, env);
- }
- }
-
/**
* @param lhoIterator Use this to read the elements of the left hand operand
* @param lho Maybe needed for operations specific to the built-in, like getting the size, otherwise use the
* {@code lhoIterator} only.
+ * @param lhoIsSequence See {@link LazilyGeneratedCollectionModel#isSequence}
* @param elementTransformer The argument to the built-in (typically a lambda expression)
*
* @return {@link TemplateSequenceModel} or {@link TemplateCollectionModel} or {@link TemplateModelIterator}.
*/
protected abstract TemplateModel calculateResult(
- TemplateModelIterator lhoIterator, TemplateModel lho, ElementTransformer elementTransformer,
+ TemplateModelIterator lhoIterator, TemplateModel lho, boolean lhoIsSequence,
+ ElementTransformer elementTransformer,
Environment env) throws TemplateException;
/**
@@ -1164,9 +1197,13 @@ class BuiltInsForSequences {
protected TemplateModel calculateResult(
final TemplateModelIterator lhoIterator, final TemplateModel lho,
- final ElementTransformer elementTransformer,
+ boolean lhoIsSequence, final ElementTransformer elementTransformer,
final Environment env) throws TemplateException {
if (!isLazilyGeneratedResultEnabled()) {
+ if (!lhoIsSequence) {
+ throw _MessageUtil.newLazilyGeneratedCollectionMustBeSequenceException(filterBI.this);
+ }
+
List<TemplateModel> resultList = new ArrayList<TemplateModel>();
while (lhoIterator.hasNext()) {
TemplateModel element = lhoIterator.next();
@@ -1176,7 +1213,7 @@ class BuiltInsForSequences {
}
return new TemplateModelListSequence(resultList);
} else {
- return new LazilyGeneratedSequenceModel(
+ return new LazilyGeneratedCollectionModelWithUnknownSize(
new TemplateModelIterator() {
boolean prefetchDone;
TemplateModel prefetchedElement;
@@ -1223,7 +1260,8 @@ class BuiltInsForSequences {
} while (!conclusionReached);
prefetchDone = true;
}
- }
+ },
+ lhoIsSequence
);
}
}
@@ -1250,9 +1288,13 @@ class BuiltInsForSequences {
static class mapBI extends IntermediateStreamOperationLikeBuiltIn {
protected TemplateModel calculateResult(
- final TemplateModelIterator lhoIterator, TemplateModel lho, final ElementTransformer elementTransformer,
+ final TemplateModelIterator lhoIterator, TemplateModel lho, boolean lhoIsSequence, final ElementTransformer elementTransformer,
final Environment env) throws TemplateException {
if (!isLazilyGeneratedResultEnabled()) {
+ if (!lhoIsSequence) {
+ throw _MessageUtil.newLazilyGeneratedCollectionMustBeSequenceException(mapBI.this);
+ }
+
List<TemplateModel> resultList = new ArrayList<TemplateModel>();
while (lhoIterator.hasNext()) {
resultList.add(fetchAndMapNextElement(lhoIterator, elementTransformer, env));
@@ -1273,12 +1315,14 @@ class BuiltInsForSequences {
}
};
if (lho instanceof TemplateCollectionModelEx) { // Preferred branch, as TempCollModEx has isEmpty() too
- return new SameSizeCollLazilyGeneratedSequenceModel(mappedLhoIterator,
- (TemplateCollectionModelEx) lho);
+ return new LazilyGeneratedCollectionModelWithSameSizeCollEx(
+ mappedLhoIterator, (TemplateCollectionModelEx) lho, lhoIsSequence);
} else if (lho instanceof TemplateSequenceModel) {
- return new SameSizeSeqLazilyGeneratedSequenceModel(mappedLhoIterator, (TemplateSequenceModel) lho);
+ return new LazilyGeneratedCollectionModelWithSameSizeSeq(
+ mappedLhoIterator, (TemplateSequenceModel) lho);
} else {
- return new LazilyGeneratedSequenceModel(mappedLhoIterator);
+ return new LazilyGeneratedCollectionModelWithUnknownSize(
+ mappedLhoIterator, lhoIsSequence);
}
}
}
diff --git a/src/main/java/freemarker/core/DynamicKeyName.java b/src/main/java/freemarker/core/DynamicKeyName.java
index 7b57983..4a72543 100644
--- a/src/main/java/freemarker/core/DynamicKeyName.java
+++ b/src/main/java/freemarker/core/DynamicKeyName.java
@@ -114,8 +114,12 @@ final class DynamicKeyName extends Expression {
}
return index < size ? tsm.get(index) : null;
}
- if (targetModel instanceof LazilyGeneratedSequenceModel) {
- TemplateModelIterator iter = ((LazilyGeneratedSequenceModel) targetModel).iterator();
+ if (targetModel instanceof LazilyGeneratedCollectionModel
+ && ((LazilyGeneratedCollectionModel) targetModel).isSequence()) {
+ if (index < 0) {
+ return null;
+ }
+ TemplateModelIterator iter = ((LazilyGeneratedCollectionModel) targetModel).iterator();
for (int curIndex = 0; iter.hasNext(); curIndex++) {
TemplateModel next = iter.next();
if (index == curIndex) {
@@ -167,15 +171,16 @@ final class DynamicKeyName extends Expression {
throws TemplateException {
// We can have 3 kind of left hand operands ("targets"): sequence, lazily generated sequence, string
final TemplateSequenceModel targetSeq;
- final LazilyGeneratedSequenceModel targetLazySeq;
+ final LazilyGeneratedCollectionModel targetLazySeq;
final String targetStr;
if (targetModel instanceof TemplateSequenceModel) {
targetSeq = (TemplateSequenceModel) targetModel;
targetLazySeq = null;
targetStr = null;
- } else if (targetModel instanceof LazilyGeneratedSequenceModel) {
+ } else if (targetModel instanceof LazilyGeneratedCollectionModel
+ && ((LazilyGeneratedCollectionModel) targetModel).isSequence()) {
targetSeq = null;
- targetLazySeq = (LazilyGeneratedSequenceModel) targetModel;
+ targetLazySeq = (LazilyGeneratedCollectionModel) targetModel;
targetStr = null;
} else {
targetSeq = null;
@@ -363,8 +368,8 @@ final class DynamicKeyName extends Expression {
}
};
return resultSize != UNKNOWN_RESULT_SIZE && targetSizeKnown // targetSizeKnown => range end was validated
- ? new LazilyGeneratedSequenceModelWithSize(iterator, resultSize)
- : new LazilyGeneratedSequenceModel(iterator);
+ ? new LazilyGeneratedCollectionModelWithAlreadyKnownSize(iterator, resultSize, true)
+ : new LazilyGeneratedCollectionModelWithUnknownSize(iterator, true);
} else { // !lazilyGeneratedResultEnabled
List<TemplateModel> resultList = resultSize != UNKNOWN_RESULT_SIZE
? new ArrayList<TemplateModel>(resultSize)
diff --git a/src/main/java/freemarker/core/LazilyGeneratedCollectionModel.java b/src/main/java/freemarker/core/LazilyGeneratedCollectionModel.java
new file mode 100644
index 0000000..76e42e3
--- /dev/null
+++ b/src/main/java/freemarker/core/LazilyGeneratedCollectionModel.java
@@ -0,0 +1,80 @@
+/*
+ * 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.core;
+
+import freemarker.template.TemplateCollectionModel;
+import freemarker.template.TemplateModelIterator;
+import freemarker.template.TemplateSequenceModel;
+
+/**
+ * Similar to {@link SingleIterationCollectionModel}, but marks the value as something that uses lazy evaluation
+ * internally, whose laziness should be transparent for the user (in FM2 at least). Values of this type shouldn't be
+ * stored without ensuring that the context needed for the generation of the lazily generated elements will be still
+ * available when the elements are read. The primary reason for the existence of this class is
+ * {@link LocalLambdaExpression}-s, which don't capture the variables they refer to, so for sequences filter/mapped
+ * by them we must ensure that all the elements are consumed before the referred variables go out of scope.
+ * <p>
+ * An operator or built-in should only ever receive a {@link LazilyGeneratedCollectionModel} if it has explicitly
+ * allowed its input expression to return such value via calling {@link Expression#enableLazilyGeneratedResult()}
+ * during parsing. With other words, an operator or built-in should only ever return
+ * {@link LazilyGeneratedCollectionModel} if it its {@link Expression#enableLazilyGeneratedResult()} method was
+ * called during the parsing.
+ * <p>
+ * Note that by accepting {@link LazilyGeneratedCollectionModel}-s the operator/built-in also undertakes taking
+ * {@link #isSequence()} into account.
+ */
+abstract class LazilyGeneratedCollectionModel extends SingleIterationCollectionModel {
+
+ private final boolean sequence;
+
+ /**
+ * @param iterator The iterator to read all the elements of this lazily generated collection.
+ * @param sequence see {@link #isSequence()}
+ */
+ protected LazilyGeneratedCollectionModel(TemplateModelIterator iterator, boolean sequence) {
+ super(iterator);
+ this.sequence = sequence;
+ }
+
+ /**
+ * If this collection is a sequence according the template author (and we only use {@link TemplateCollectionModel}
+ * internally to implement lazy generation). That means that an operator or built-in that accepts sequences must
+ * accept this {@link TemplateCollectionModel} value, instead of giving a type error. This of course only applies
+ * to operators/built-ins that accept lazy values on the first place (see
+ * {@link Expression#enableLazilyGeneratedResult}). Such operators/built-ins must implement their functionality
+ * with {@link TemplateCollectionModel} input as well, in additionally to the normal implementation with
+ * {@link TemplateSequenceModel} input. If {@link #isSequence()} returns {@code false}, and the operator/built-in
+ * doesn't support {@link TemplateCollectionModel} in general, it must fail with type error.
+ */
+ final boolean isSequence() {
+ return sequence;
+ }
+
+ /**
+ * Returns a "view" of this {@link LazilyGeneratedCollectionModel} where {@link #isSequence()} returns
+ * @code true}.
+ */
+ final LazilyGeneratedCollectionModel withIsSequenceTrue() {
+ return isSequence() ? this : withIsSequenceFromFalseToTrue();
+ }
+
+ protected abstract LazilyGeneratedCollectionModel withIsSequenceFromFalseToTrue();
+
+}
diff --git a/src/main/java/freemarker/core/LazilyGeneratedSequenceModel.java b/src/main/java/freemarker/core/LazilyGeneratedCollectionModelEx.java
similarity index 52%
rename from src/main/java/freemarker/core/LazilyGeneratedSequenceModel.java
rename to src/main/java/freemarker/core/LazilyGeneratedCollectionModelEx.java
index 26e210a..a162768 100644
--- a/src/main/java/freemarker/core/LazilyGeneratedSequenceModel.java
+++ b/src/main/java/freemarker/core/LazilyGeneratedCollectionModelEx.java
@@ -19,19 +19,15 @@
package freemarker.core;
-import freemarker.ext.beans.CollectionModel;
+import freemarker.template.TemplateCollectionModelEx;
import freemarker.template.TemplateModelIterator;
-import freemarker.template.TemplateSequenceModel;
/**
- * Same as {@link SingleIterationCollectionModel}, but marks the value as something that's in principle a
- * {@link TemplateSequenceModel}, but to allow lazy result generation a {@link CollectionModel} is used internally.
- * This is an optimization that we do where we consider it to be transparent enough for the user. An operator or
- * built-in should only ever receive a {@link LazilyGeneratedSequenceModel} if it has explicitly allowed its
- * input expression to return such value via calling {@link Expression#enableLazilyGeneratedResult()}.
+ * A {@link LazilyGeneratedCollectionModel} that supports {@link TemplateCollectionModelEx} methods.
*/
-class LazilyGeneratedSequenceModel extends SingleIterationCollectionModel {
- LazilyGeneratedSequenceModel(TemplateModelIterator iterator) {
- super(iterator);
+abstract class LazilyGeneratedCollectionModelEx extends LazilyGeneratedCollectionModel implements
+ TemplateCollectionModelEx {
+ LazilyGeneratedCollectionModelEx(TemplateModelIterator iterator, boolean sequence) {
+ super(iterator, sequence);
}
}
diff --git a/src/main/java/freemarker/core/LazilyGeneratedSequenceModelWithSize.java b/src/main/java/freemarker/core/LazilyGeneratedCollectionModelWithAlreadyKnownSize.java
similarity index 71%
copy from src/main/java/freemarker/core/LazilyGeneratedSequenceModelWithSize.java
copy to src/main/java/freemarker/core/LazilyGeneratedCollectionModelWithAlreadyKnownSize.java
index 7faab2e..99df53f 100644
--- a/src/main/java/freemarker/core/LazilyGeneratedSequenceModelWithSize.java
+++ b/src/main/java/freemarker/core/LazilyGeneratedCollectionModelWithAlreadyKnownSize.java
@@ -19,15 +19,14 @@
package freemarker.core;
-import freemarker.template.TemplateCollectionModelEx;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateModelIterator;
-class LazilyGeneratedSequenceModelWithSize extends LazilyGeneratedSequenceModel implements TemplateCollectionModelEx {
+final class LazilyGeneratedCollectionModelWithAlreadyKnownSize extends LazilyGeneratedCollectionModelEx {
private final int size;
- LazilyGeneratedSequenceModelWithSize(TemplateModelIterator iterator, int size) {
- super(iterator);
+ LazilyGeneratedCollectionModelWithAlreadyKnownSize(TemplateModelIterator iterator, int size, boolean sequence) {
+ super(iterator, sequence);
this.size = size;
}
@@ -38,4 +37,9 @@ class LazilyGeneratedSequenceModelWithSize extends LazilyGeneratedSequenceModel
public boolean isEmpty() {
return size == 0;
}
+
+ @Override
+ protected LazilyGeneratedCollectionModel withIsSequenceFromFalseToTrue() {
+ return new LazilyGeneratedCollectionModelWithAlreadyKnownSize(getIterator(), size, true);
+ }
}
diff --git a/src/main/java/freemarker/core/SameSizeLazilyGeneratedSequenceModel.java b/src/main/java/freemarker/core/LazilyGeneratedCollectionModelWithSameSizeCollEx.java
similarity index 72%
rename from src/main/java/freemarker/core/SameSizeLazilyGeneratedSequenceModel.java
rename to src/main/java/freemarker/core/LazilyGeneratedCollectionModelWithSameSizeCollEx.java
index a0f4225..6917273 100644
--- a/src/main/java/freemarker/core/SameSizeLazilyGeneratedSequenceModel.java
+++ b/src/main/java/freemarker/core/LazilyGeneratedCollectionModelWithSameSizeCollEx.java
@@ -25,16 +25,15 @@ import freemarker.template.TemplateModelIterator;
import freemarker.template.utility.NullArgumentException;
/**
- * Used instead of {@link LazilyGeneratedSequenceModel} for operations that don't change the element count of the
+ * Used instead of {@link LazilyGeneratedCollectionModel} for operations that don't change the element count of the
* source, if the source can also give back an element count.
*/
-class SameSizeCollLazilyGeneratedSequenceModel extends LazilyGeneratedSequenceModel
- implements TemplateCollectionModelEx {
+class LazilyGeneratedCollectionModelWithSameSizeCollEx extends LazilyGeneratedCollectionModelEx {
private final TemplateCollectionModelEx sizeSourceCollEx;
- public SameSizeCollLazilyGeneratedSequenceModel(
- TemplateModelIterator iterator, TemplateCollectionModelEx sizeSourceCollEx) {
- super(iterator);
+ public LazilyGeneratedCollectionModelWithSameSizeCollEx(
+ TemplateModelIterator iterator, TemplateCollectionModelEx sizeSourceCollEx, boolean sequenceSourced) {
+ super(iterator, sequenceSourced);
NullArgumentException.check(sizeSourceCollEx);
this.sizeSourceCollEx = sizeSourceCollEx;
}
@@ -46,4 +45,9 @@ class SameSizeCollLazilyGeneratedSequenceModel extends LazilyGeneratedSequenceMo
public boolean isEmpty() throws TemplateModelException {
return sizeSourceCollEx.isEmpty();
}
+
+ @Override
+ protected LazilyGeneratedCollectionModelWithSameSizeCollEx withIsSequenceFromFalseToTrue() {
+ return new LazilyGeneratedCollectionModelWithSameSizeCollEx(getIterator(), sizeSourceCollEx, true);
+ }
}
diff --git a/src/main/java/freemarker/core/SameSizeSeqLazilyGeneratedSequenceModel.java b/src/main/java/freemarker/core/LazilyGeneratedCollectionModelWithSameSizeSeq.java
similarity index 77%
rename from src/main/java/freemarker/core/SameSizeSeqLazilyGeneratedSequenceModel.java
rename to src/main/java/freemarker/core/LazilyGeneratedCollectionModelWithSameSizeSeq.java
index d6d7b5f..15e7c4a 100644
--- a/src/main/java/freemarker/core/SameSizeSeqLazilyGeneratedSequenceModel.java
+++ b/src/main/java/freemarker/core/LazilyGeneratedCollectionModelWithSameSizeSeq.java
@@ -19,25 +19,21 @@
package freemarker.core;
-import freemarker.template.TemplateCollectionModelEx;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateModelIterator;
import freemarker.template.TemplateSequenceModel;
import freemarker.template.utility.NullArgumentException;
/**
- * Used instead of {@link LazilyGeneratedSequenceModel} for operations that don't change the element count of the
+ * Used instead of {@link LazilyGeneratedCollectionModel} for operations that don't change the element count of the
* source, if the source can also give back an element count.
- *
- * @since 2.3.29
*/
-class SameSizeSeqLazilyGeneratedSequenceModel extends LazilyGeneratedSequenceModel
- implements TemplateCollectionModelEx {
+class LazilyGeneratedCollectionModelWithSameSizeSeq extends LazilyGeneratedCollectionModelEx {
private final TemplateSequenceModel sizeSourceSeq;
- public SameSizeSeqLazilyGeneratedSequenceModel(
+ public LazilyGeneratedCollectionModelWithSameSizeSeq(
TemplateModelIterator iterator, TemplateSequenceModel sizeSourceSeq) {
- super(iterator);
+ super(iterator, true);
NullArgumentException.check(sizeSourceSeq);
this.sizeSourceSeq = sizeSourceSeq;
}
@@ -49,4 +45,9 @@ class SameSizeSeqLazilyGeneratedSequenceModel extends LazilyGeneratedSequenceMod
public boolean isEmpty() throws TemplateModelException {
return sizeSourceSeq.size() == 0;
}
+
+ @Override
+ protected LazilyGeneratedCollectionModelWithSameSizeSeq withIsSequenceFromFalseToTrue() {
+ return this; // Won't be actually called...
+ }
}
diff --git a/src/main/java/freemarker/core/LazilyGeneratedSequenceModelWithSize.java b/src/main/java/freemarker/core/LazilyGeneratedCollectionModelWithUnknownSize.java
similarity index 63%
rename from src/main/java/freemarker/core/LazilyGeneratedSequenceModelWithSize.java
rename to src/main/java/freemarker/core/LazilyGeneratedCollectionModelWithUnknownSize.java
index 7faab2e..53ec1e8 100644
--- a/src/main/java/freemarker/core/LazilyGeneratedSequenceModelWithSize.java
+++ b/src/main/java/freemarker/core/LazilyGeneratedCollectionModelWithUnknownSize.java
@@ -19,23 +19,15 @@
package freemarker.core;
-import freemarker.template.TemplateCollectionModelEx;
-import freemarker.template.TemplateModelException;
import freemarker.template.TemplateModelIterator;
-class LazilyGeneratedSequenceModelWithSize extends LazilyGeneratedSequenceModel implements TemplateCollectionModelEx {
- private final int size;
-
- LazilyGeneratedSequenceModelWithSize(TemplateModelIterator iterator, int size) {
- super(iterator);
- this.size = size;
- }
-
- public int size() throws TemplateModelException {
- return size;
+public final class LazilyGeneratedCollectionModelWithUnknownSize extends LazilyGeneratedCollectionModel {
+ public LazilyGeneratedCollectionModelWithUnknownSize(TemplateModelIterator iterator, boolean sequence) {
+ super(iterator, sequence);
}
- public boolean isEmpty() {
- return size == 0;
+ @Override
+ protected LazilyGeneratedCollectionModelWithUnknownSize withIsSequenceFromFalseToTrue() {
+ return new LazilyGeneratedCollectionModelWithUnknownSize(getIterator(), true);
}
}
diff --git a/src/main/java/freemarker/core/SingleIterationCollectionModel.java b/src/main/java/freemarker/core/SingleIterationCollectionModel.java
index 56efe93..f8e8471 100644
--- a/src/main/java/freemarker/core/SingleIterationCollectionModel.java
+++ b/src/main/java/freemarker/core/SingleIterationCollectionModel.java
@@ -48,4 +48,8 @@ class SingleIterationCollectionModel implements TemplateCollectionModel {
iterator = null;
return result;
}
+
+ protected TemplateModelIterator getIterator() {
+ return iterator;
+ }
}
diff --git a/src/main/java/freemarker/core/_CoreAPI.java b/src/main/java/freemarker/core/_CoreAPI.java
index 7b0596b..55a2d3d 100644
--- a/src/main/java/freemarker/core/_CoreAPI.java
+++ b/src/main/java/freemarker/core/_CoreAPI.java
@@ -27,6 +27,7 @@ import java.util.TreeSet;
import freemarker.template.Configuration;
import freemarker.template.Template;
+import freemarker.template.TemplateCollectionModel;
import freemarker.template.TemplateDirectiveBody;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModel;
@@ -219,7 +220,7 @@ public class _CoreAPI {
parser.setPreventStrippings(preventStrippings);
}
- public static boolean isLazilyGeneratedSequenceModel(Class cl) {
- return LazilyGeneratedSequenceModel.class.isAssignableFrom(cl);
+ public static boolean isLazilyGeneratedSequenceModel(TemplateCollectionModel model) {
+ return model instanceof LazilyGeneratedCollectionModel && ((LazilyGeneratedCollectionModel) model).isSequence();
}
}
diff --git a/src/main/java/freemarker/core/_MessageUtil.java b/src/main/java/freemarker/core/_MessageUtil.java
index 6492607..4d2df6a 100644
--- a/src/main/java/freemarker/core/_MessageUtil.java
+++ b/src/main/java/freemarker/core/_MessageUtil.java
@@ -341,22 +341,31 @@ public class _MessageUtil {
new _DelayedShortClassName(TemplateHashModelEx2.class),
", which leads to this restriction."));
}
-
+
+ public static TemplateException newLazilyGeneratedCollectionMustBeSequenceException(Expression blamed) {
+ return new _MiscTemplateException(blamed,
+ "The result is a listable value with lazy transformation(s) applied on it, but it's not " +
+ "an FTL sequence (it's not a List-like value, but an Iterator-like value). The place doesn't " +
+ "support such values due to technical limitations. So either pass it to a construct that supports " +
+ "such values (like ", "<#list transformedListable as x>", "), or, if you know that you don't have " +
+ "too many elements, use transformedListable?sequence to allow it to be treated as an FTL sequence.");
+ }
+
/**
* @return "a" or "an" or "a(n)" (or "" for empty string) for an FTL type name
*/
static public String getAOrAn(String s) {
if (s == null) return null;
if (s.length() == 0) return "";
-
+
char fc = Character.toLowerCase(s.charAt(0));
if (fc == 'a' || fc == 'e' || fc == 'i') {
return "an";
- } else if (fc == 'h') {
+ } else if (fc == 'h') {
String ls = s.toLowerCase();
- if (ls.startsWith("has") || ls.startsWith("hi")) {
+ if (ls.startsWith("has") || ls.startsWith("hi")) {
return "a";
- } else if (ls.startsWith("ht")) {
+ } else if (ls.startsWith("ht")) {
return "an";
} else {
return "a(n)";
@@ -364,7 +373,7 @@ public class _MessageUtil {
} else if (fc == 'u' || fc == 'o') {
return "a(n)";
} else {
- char sc = (s.length() > 1) ? s.charAt(1) : '\0';
+ char sc = (s.length() > 1) ? s.charAt(1) : '\0';
if (fc == 'x' && !(sc == 'a' || sc == 'e' || sc == 'i' || sc == 'a' || sc == 'o' || sc == 'u')) {
return "an";
} else {
@@ -372,5 +381,4 @@ public class _MessageUtil {
}
}
}
-
}
diff --git a/src/main/java/freemarker/template/utility/ClassUtil.java b/src/main/java/freemarker/template/utility/ClassUtil.java
index 45508df..ad19750 100644
--- a/src/main/java/freemarker/template/utility/ClassUtil.java
+++ b/src/main/java/freemarker/template/utility/ClassUtil.java
@@ -188,6 +188,9 @@ public class ClassUtil {
}
} else if (tm instanceof SimpleMethodModel || tm instanceof OverloadedMethodsModel) {
return TemplateMethodModelEx.class;
+ } else if (tm instanceof TemplateCollectionModel
+ && _CoreAPI.isLazilyGeneratedSequenceModel((TemplateCollectionModel) tm)) {
+ return TemplateSequenceModel.class;
} else {
return null;
}
@@ -208,7 +211,7 @@ public class ClassUtil {
appendTypeName(sb, typeNamesAppended, "transform");
}
- if (TemplateSequenceModel.class.isAssignableFrom(cl) || _CoreAPI.isLazilyGeneratedSequenceModel(cl)) {
+ if (TemplateSequenceModel.class.isAssignableFrom(cl)) {
appendTypeName(sb, typeNamesAppended, "sequence");
} else if (TemplateCollectionModel.class.isAssignableFrom(cl)) {
appendTypeName(sb, typeNamesAppended,
diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml
index 3285d9b..931f768 100644
--- a/src/manual/en_US/book.xml
+++ b/src/manual/en_US/book.xml
@@ -27933,6 +27933,32 @@ TemplateModel x = env.getVariable("x"); // get variable x</programlisting>
</listitem>
<listitem>
+ <para><link
+ linkend="ref_builtin_sequence"><literal>?sequence</literal></link>
+ now collaborates with
+ <literal><replaceable>seq</replaceable>?size</literal>,
+ <literal><replaceable>seq</replaceable>[<replaceable>index</replaceable>]</literal>,
+ <literal><replaceable>seq</replaceable>[<replaceable>range</replaceable>]</literal>,
+ and with some built-ins (<literal>filter</literal>,
+ <literal>map</literal>, <literal>join</literal>, etc.) to spare
+ collecting all the elements into the memory when possible. For
+ example <literal>anIterator?sequence[1]</literal> now will just
+ fetch the first 2 items, instead of building a sequence that
+ contains all the elements, and then getting the 2nd element from
+ that. Or, if you write
+ <literal>anIterator?sequence?size</literal>, it will just skip
+ through all elements to count them, but won't store them in
+ memory. These optimizations only work within the same chain of
+ built-in calls, so for example in <literal><#assign seq =
+ anIterator?sequence>${seq[1]}</literal> will still collect
+ all the elements into the memory, as
+ <literal>anIterator?sequence</literal> and
+ <literal>seq[1]</literal> are separated. [TODO: document this at
+ <link linkend="ref_builtin_sequence">ref_builtin_sequence</link>
+ too]</para>
+ </listitem>
+
+ <listitem>
<para>Extended big decimal format parameter
<quote>multiplier</quote> was incorrectly written as
<quote>multipier</quote>. Now both words are recognized.</para>
diff --git a/src/test/java/freemarker/core/FilterBiTest.java b/src/test/java/freemarker/core/FilterBiTest.java
index 14e37ac..ee16dff 100644
--- a/src/test/java/freemarker/core/FilterBiTest.java
+++ b/src/test/java/freemarker/core/FilterBiTest.java
@@ -24,7 +24,10 @@ import java.util.List;
import org.junit.Test;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import freemarker.template.Configuration;
+import freemarker.template.DefaultObjectWrapper;
import freemarker.template.TemplateException;
import freemarker.test.TemplateTest;
@@ -40,6 +43,17 @@ public class FilterBiTest extends TemplateTest {
}
}
+ @Override
+ protected Configuration createConfiguration() throws Exception {
+ Configuration cfg = super.createConfiguration();
+
+ DefaultObjectWrapper objectWrapper = new DefaultObjectWrapper(Configuration.VERSION_2_3_28);
+ objectWrapper.setForceLegacyNonListCollections(false);
+ cfg.setObjectWrapper(objectWrapper);
+
+ return cfg;
+ }
+
private static final List<TestParam> TEST_PARAMS = ImmutableList.of(
new TestParam(ImmutableList.of("a", "aX", "bX", "b", "cX", "c"), "a, b, c"),
new TestParam(ImmutableList.of("a", "b", "c"), "a, b, c"),
@@ -130,6 +144,17 @@ public class FilterBiTest extends TemplateTest {
"lambda", "1 parameter", "declared 2");
}
+ @Test
+ public void testNonSequenceInput() throws Exception {
+ addToDataModel("coll", ImmutableSet.of("a", "b", "c"));
+ assertErrorContains("${coll?filter(it -> it != 'a')[0]}", "sequence", "evaluated to a collection");
+ assertErrorContains("[#ftl][#assign t = coll?filter(it -> it != 'a')]",
+ "lazy transformation", "?sequence", "[#list");
+ assertOutput("${coll?sequence?filter(it -> it != 'a')[0]}", "b");
+ assertOutput("${coll?filter(it -> it != 'a')?sequence[0]}", "b");
+ assertOutput("<#list coll?filter(it -> it != 'a') as it>${it}</#list>", "bc");
+ }
+
public static class FilterObject {
public boolean noX(String s) {
return !s.contains("X");
diff --git a/src/test/java/freemarker/core/LazilyGeneratedSeqTest.java b/src/test/java/freemarker/core/LazilyGeneratedCollectionTest.java
similarity index 80%
rename from src/test/java/freemarker/core/LazilyGeneratedSeqTest.java
rename to src/test/java/freemarker/core/LazilyGeneratedCollectionTest.java
index 0886f05..fbbf226 100644
--- a/src/test/java/freemarker/core/LazilyGeneratedSeqTest.java
+++ b/src/test/java/freemarker/core/LazilyGeneratedCollectionTest.java
@@ -28,6 +28,7 @@ import java.util.List;
import org.junit.Test;
import freemarker.template.Configuration;
+import freemarker.template.DefaultObjectWrapper;
import freemarker.template.SimpleNumber;
import freemarker.template.TemplateCollectionModel;
import freemarker.template.TemplateCollectionModelEx;
@@ -38,33 +39,67 @@ import freemarker.template.TemplateSequenceModel;
import freemarker.test.TemplateTest;
/**
- * Tests operators and built-ins that are support getting {@link LazilyGeneratedSequenceModel} as their operands.
+ * Tests operators and built-ins that are support getting and/or returning {@link LazilyGeneratedCollectionModel}.
+ * @see MapBiTest
+ * @see FilterBiTest
*/
-public class LazilyGeneratedSeqTest extends TemplateTest {
+public class LazilyGeneratedCollectionTest extends TemplateTest {
+
+ @Override
+ protected Configuration createConfiguration() throws Exception {
+ Configuration cfg = super.createConfiguration();
+ cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_29);
+ cfg.setBooleanFormat("c");
+ cfg.setSharedVariable("seq", new MonitoredTemplateSequenceModel(1, 2, 3));
+ cfg.setSharedVariable("seqLong", new MonitoredTemplateSequenceModel(1, 2, 3, 4, 5, 6));
+ cfg.setSharedVariable("coll", new MonitoredTemplateCollectionModel(1, 2, 3));
+ cfg.setSharedVariable("collLong", new MonitoredTemplateCollectionModel(1, 2, 3, 4, 5, 6));
+ cfg.setSharedVariable("collEx", new MonitoredTemplateCollectionModelEx(1, 2, 3));
+
+ DefaultObjectWrapper objectWrapper = new DefaultObjectWrapper(Configuration.VERSION_2_3_28);
+ objectWrapper.setForceLegacyNonListCollections(false);
+ cfg.setObjectWrapper(objectWrapper);
+
+ return cfg;
+ }
@Test
public void dynamicIndexTest() throws Exception {
- assertErrorContains("${coll?map(it -> it)['x']}",
+ assertErrorContains("${coll?sequence?map(it -> it)['x']}",
"hash", "evaluated to a sequence");
- assertOutput("${coll?map(it -> it)[0]}",
+ assertOutput("${coll?sequence?map(it -> it)[0]}",
"[iterator][hasNext][next]1");
- assertOutput("${coll?map(it -> it)[1]}",
+ assertOutput("${coll?sequence?map(it -> it)[1]}",
"[iterator][hasNext][next][hasNext][next]2");
- assertOutput("${coll?map(it -> it)[2]}",
+ assertOutput("${coll?map(it -> it)?sequence[1]}",
+ "[iterator][hasNext][next][hasNext][next]2");
+ assertOutput("${coll?sequence?map(it -> it)[2]}",
"[iterator][hasNext][next][hasNext][next][hasNext][next]3");
- assertOutput("${coll?map(it -> it)[3]!'missing'}",
+ assertOutput("${coll?sequence?map(it -> it)[3]!'missing'}",
"[iterator][hasNext][next][hasNext][next][hasNext][next][hasNext]missing");
- assertOutput("${coll?filter(it -> it % 2 == 0)[0]}",
+ assertOutput("${coll?sequence?filter(it -> it % 2 == 0)[0]}",
"[iterator][hasNext][next][hasNext][next]2");
- assertOutput("${coll?filter(it -> it > 3)[0]!'missing'}",
+ assertOutput("${coll?sequence?filter(it -> it > 3)[0]!'missing'}",
"[iterator][hasNext][next][hasNext][next][hasNext][next][hasNext]missing");
- assertOutput("${collLong?map(it -> it)[1 .. 2]?join(', ')}",
+ assertOutput("${collLong?sequence?map(it -> it)[1 .. 2]?join(', ')}",
"[iterator][hasNext][next][hasNext][next][hasNext][next]2, 3");
}
@Test
+ public void dynamicIndexNonSequenceInput() throws Exception {
+ assertErrorContains("${coll[1]}", "sequence", "evaluated to a collection");
+ assertOutput("${coll?sequence[1]}", "[iterator][hasNext][next][hasNext][next]2");
+
+ assertErrorContains("<#assign t = coll[1..2]>", "sequence", "evaluated to a collection");
+ assertOutput("<#assign t = coll?sequence[1..2]>${t?join('')}",
+ "[iterator][hasNext][next][hasNext][next][hasNext][next]23");
+ assertOutput("<#list coll?sequence[1..2] as it>${it}</#list>",
+ "[iterator][hasNext][next][hasNext][next]2[hasNext][next]3");
+ }
+
+ @Test
public void sizeBasicsTest() throws Exception {
assertOutput("${seq?size}",
"[size]3");
@@ -75,12 +110,16 @@ public class LazilyGeneratedSeqTest extends TemplateTest {
assertOutput("${seq?map(x -> x * 10)?size}",
"[size]3");
- assertOutput("${collEx?map(x -> x * 10)?size}",
+ assertOutput("${collEx?sequence?map(x -> x * 10)?size}",
+ "[size]3");
+ assertOutput("${collEx?map(x -> x * 10)?sequence?size}",
"[size]3");
assertOutput("${seq?filter(x -> x != 1)?size}",
"[size][get 0][get 1][get 2]2");
- assertOutput("${collEx?filter(x -> x != 1)?size}",
+ assertOutput("${collEx?sequence?filter(x -> x != 1)?size}",
+ "[iterator][hasNext][next][hasNext][next][hasNext][next][hasNext]2");
+ assertOutput("${collEx?filter(x -> x != 1)?sequence?size}",
"[iterator][hasNext][next][hasNext][next][hasNext][next][hasNext]2");
}
@@ -119,22 +158,30 @@ public class LazilyGeneratedSeqTest extends TemplateTest {
"[size]false");
// Now the lazy generation things:
- assertOutput("${collLong?filter(x -> true)?size}",
+ assertOutput("${collLong?sequence?filter(x -> true)?size}",
"[iterator]" +
"[hasNext][next][hasNext][next][hasNext][next]" +
"[hasNext][next][hasNext][next][hasNext][next][hasNext]6");
// Note: "[next]" is added by ?filter, as it has to know if the element matches the predicate.
- assertOutput("${collLong?filter(x -> true)?size != 0}",
+ assertOutput("${collLong?sequence?filter(x -> true)?size != 0}",
"[iterator][hasNext][next]true");
- assertOutput("${collLong?filter(x -> true)?size != 1}",
+ assertOutput("${collLong?sequence?filter(x -> true)?size != 1}",
"[iterator][hasNext][next][hasNext][next]true");
- assertOutput("${collLong?filter(x -> true)?size == 1}",
+ assertOutput("${collLong?sequence?filter(x -> true)?size == 1}",
+ "[iterator][hasNext][next][hasNext][next]false");
+ assertOutput("${collLong?filter(x -> true)?sequence?size == 1}",
"[iterator][hasNext][next][hasNext][next]false");
- assertOutput("${collLong?filter(x -> true)?size < 3}",
+ assertOutput("${collLong?sequence?filter(x -> true)?size < 3}",
"[iterator][hasNext][next][hasNext][next][hasNext][next]false");
}
@Test
+ public void sizeNonSequenceInput() throws Exception {
+ assertErrorContains("${coll?size}", "sequence", "evaluated to a collection");
+ assertOutput("${coll?sequence?size}", "[iterator][hasNext][next][hasNext][next][hasNext][next][hasNext]3");
+ }
+
+ @Test
public void firstTest() throws Exception {
assertOutput("${coll?first}",
"[iterator][hasNext][next]1");
@@ -218,19 +265,6 @@ public class LazilyGeneratedSeqTest extends TemplateTest {
assertOutput("${seqLong?filter(x->true)[2..*3]?size}", "[size][get 0][get 1][get 2][get 3][get 4]3");
}
- @Override
- protected Configuration createConfiguration() throws Exception {
- Configuration cfg = super.createConfiguration();
- cfg.setIncompatibleImprovements(Configuration.VERSION_2_3_29);
- cfg.setBooleanFormat("c");
- cfg.setSharedVariable("seq", new MonitoredTemplateSequenceModel(1, 2, 3));
- cfg.setSharedVariable("seqLong", new MonitoredTemplateSequenceModel(1, 2, 3, 4, 5, 6));
- cfg.setSharedVariable("coll", new MonitoredTemplateCollectionModel(1, 2, 3));
- cfg.setSharedVariable("collLong", new MonitoredTemplateCollectionModel(1, 2, 3, 4, 5, 6));
- cfg.setSharedVariable("collEx", new MonitoredTemplateCollectionModelEx(1, 2, 3));
- return cfg;
- }
-
public static abstract class ListContainingTemplateModel {
protected final List<Number> elements;
diff --git a/src/test/java/freemarker/core/MapBiTest.java b/src/test/java/freemarker/core/MapBiTest.java
index 49934be..3e70c0f 100644
--- a/src/test/java/freemarker/core/MapBiTest.java
+++ b/src/test/java/freemarker/core/MapBiTest.java
@@ -25,8 +25,10 @@ import java.util.List;
import org.junit.Test;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
import freemarker.template.Configuration;
+import freemarker.template.DefaultObjectWrapper;
import freemarker.template.TemplateException;
import freemarker.test.TemplateTest;
@@ -51,8 +53,14 @@ public class MapBiTest extends TemplateTest {
@Override
protected Configuration createConfiguration() throws Exception {
Configuration cfg = super.createConfiguration();
+
cfg.setNumberFormat("0.####");
cfg.setBooleanFormat("c");
+
+ DefaultObjectWrapper objectWrapper = new DefaultObjectWrapper(Configuration.VERSION_2_3_28);
+ objectWrapper.setForceLegacyNonListCollections(false);
+ cfg.setObjectWrapper(objectWrapper);
+
return cfg;
}
@@ -181,6 +189,17 @@ public class MapBiTest extends TemplateTest {
"lambda", "1 parameter", "declared 2");
}
+ @Test
+ public void testNonSequenceInput() throws Exception {
+ addToDataModel("coll", ImmutableSet.of("a", "b", "c"));
+ assertErrorContains("${coll?map(it -> it?upperCase)[0]}", "sequence", "evaluated to an extended_collection");
+ assertErrorContains("[#ftl][#assign t = coll?map(it -> it?upperCase)]",
+ "lazy transformation", "?sequence", "[#list");
+ assertOutput("${coll?sequence?map(it -> it?upperCase)[0]}", "A");
+ assertOutput("${coll?map(it -> it?upperCase)?sequence[0]}", "A");
+ assertOutput("<#list coll?map(it -> it?upperCase) as it>${it}</#list>", "ABC");
+ }
+
public static class MapperObject {
public String toUpper(String s) {
return s.toUpperCase();