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

commit abaed15caf0d3e43da2d1e6abfecbda7b03c20f9
Author: ddekany <[email protected]>
AuthorDate: Sun Mar 31 16:43:31 2019 +0200

    Some cleanup related to local lambdas, ?filter and ?map. Added 
optimizations to ?size operating on the result of ?filter/?map.
---
 src/main/java/freemarker/core/BuiltIn.java         |   6 +-
 .../freemarker/core/BuiltInsForMultipleTypes.java  |  21 +++
 .../java/freemarker/core/BuiltInsForSequences.java | 101 +++++++-----
 src/main/java/freemarker/core/Environment.java     |   2 -
 src/main/java/freemarker/core/IteratorBlock.java   |   4 +-
 .../core/LazilyGeneratedSequenceModel.java         |  35 +++++
 .../core/LazyCollectionTemplateModelIterator.java  |  54 +++++++
 .../java/freemarker/core/LazySequenceIterator.java |  55 +++++++
 .../core/SameSizeLazilyGeneratedSequenceModel.java |  49 ++++++
 .../SameSizeSeqLazilyGeneratedSequenceModel.java   |  52 ++++++
 ...ilyGeneratedSeqTargetSupportInBuiltinsTest.java | 174 +++++++++++++++++++++
 .../core/ListWithStreamLikeBuiltinsTest.java       |   2 +-
 12 files changed, 507 insertions(+), 48 deletions(-)

diff --git a/src/main/java/freemarker/core/BuiltIn.java 
b/src/main/java/freemarker/core/BuiltIn.java
index 450dd6d..0679e21 100644
--- a/src/main/java/freemarker/core/BuiltIn.java
+++ b/src/main/java/freemarker/core/BuiltIn.java
@@ -387,16 +387,16 @@ abstract class BuiltIn extends Expression implements 
Cloneable {
         }
         bi.key = key;
         bi.target = target;
-        if (bi.isSingleIterationCollectionTargetSupported()) {
+        if (bi.isLazilyGeneratedSequenceModelTargetSupported()) {
             if (target instanceof 
BuiltInsForSequences.IntermediateStreamOperationLikeBuiltIn) {
                 ((BuiltInsForSequences.IntermediateStreamOperationLikeBuiltIn) 
target)
-                        .setLazyProcessingAllowed(true);
+                        .setLazyResultGenerationAllowed(true);
             }
         }
         return bi;
     }
 
-    protected boolean isSingleIterationCollectionTargetSupported() {
+    protected boolean isLazilyGeneratedSequenceModelTargetSupported() {
         return false;
     }
 
diff --git a/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java 
b/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
index e0640c2..61f8f3f 100644
--- a/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
+++ b/src/main/java/freemarker/core/BuiltInsForMultipleTypes.java
@@ -40,6 +40,7 @@ import freemarker.template.TemplateHashModelEx;
 import freemarker.template.TemplateMethodModel;
 import freemarker.template.TemplateModel;
 import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateModelIterator;
 import freemarker.template.TemplateModelWithAPISupport;
 import freemarker.template.TemplateNodeModel;
 import freemarker.template.TemplateNumberModel;
@@ -481,6 +482,12 @@ class BuiltInsForMultipleTypes {
     }
 
     static class sizeBI extends BuiltIn {
+
+        @Override
+        protected boolean isLazilyGeneratedSequenceModelTargetSupported() {
+            return true;
+        }
+
         @Override
         TemplateModel _eval(Environment env) throws TemplateException {
             TemplateModel model = target.eval(env);
@@ -492,6 +499,20 @@ class BuiltInsForMultipleTypes {
                 size = ((TemplateCollectionModelEx) model).size();
             } else if (model instanceof TemplateHashModelEx) {
                 size = ((TemplateHashModelEx) model).size();
+            } else if (model instanceof LazilyGeneratedSequenceModel) {
+                // 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
+                // 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();
+                int counter = 0;
+                while (iterator.hasNext()) {
+                    counter++;
+                    iterator.next();
+                }
+                size = counter;
             } else {
                 throw new UnexpectedTypeException(
                         target, model,
diff --git a/src/main/java/freemarker/core/BuiltInsForSequences.java 
b/src/main/java/freemarker/core/BuiltInsForSequences.java
index 0bb2bd9..b4acd16 100644
--- a/src/main/java/freemarker/core/BuiltInsForSequences.java
+++ b/src/main/java/freemarker/core/BuiltInsForSequences.java
@@ -144,7 +144,7 @@ class BuiltInsForSequences {
     static class firstBI extends BuiltIn {
 
         @Override
-        protected boolean isSingleIterationCollectionTargetSupported() {
+        protected boolean isLazilyGeneratedSequenceModelTargetSupported() {
             return true;
         }
 
@@ -185,7 +185,7 @@ class BuiltInsForSequences {
     static class joinBI extends BuiltIn {
 
         @Override
-        protected boolean isSingleIterationCollectionTargetSupported() {
+        protected boolean isLazilyGeneratedSequenceModelTargetSupported() {
             return true;
         }
 
@@ -302,7 +302,7 @@ class BuiltInsForSequences {
     static class seq_containsBI extends BuiltIn {
 
         @Override
-        protected boolean isSingleIterationCollectionTargetSupported() {
+        protected boolean isLazilyGeneratedSequenceModelTargetSupported() {
             return true;
         }
 
@@ -374,7 +374,7 @@ class BuiltInsForSequences {
     static class seq_index_ofBI extends BuiltIn {
 
         @Override
-        protected boolean isSingleIterationCollectionTargetSupported() {
+        protected boolean isLazilyGeneratedSequenceModelTargetSupported() {
             return true;
         }
 
@@ -915,7 +915,7 @@ class BuiltInsForSequences {
         }
 
         @Override
-        protected boolean isSingleIterationCollectionTargetSupported() {
+        protected boolean isLazilyGeneratedSequenceModelTargetSupported() {
             return true;
         }
 
@@ -980,19 +980,20 @@ class BuiltInsForSequences {
     }
 
     /**
-     * Built-in that's similar to an Java 8 Stream intermediate operation. To 
be on the safe side, by default these
-     * are eager, and just produce a {@link TemplateSequenceModel}. But when 
circumstances allow, they become
-     * lazy, similarly to Java 8 Stream-s. Another characteristic of the 
built-ins that they usually accept
-     * lambda expressions as parameters.
+     * Built-in that's similar to a Java 8 Stream intermediate operation. To 
be on the safe side, by default these
+     * are eager, and just produce a {@link TemplateSequenceModel}. But when 
circumstances allow, they become lazy,
+     * similarly to Java 8 Stream intermediate operations. Another 
characteristic of these built-ins is that they
+     * usually accept lambda expressions as parameters.
      */
     static abstract class IntermediateStreamOperationLikeBuiltIn extends 
BuiltInWithParseTimeParameters {
 
         private Expression elementTransformerExp;
         private ElementTransformer precreatedElementTransformer;
-        private boolean lazyProcessingAllowed;
+        private boolean lazyResultGenerationAllowed;
 
         @Override
         void bindToParameters(List<Expression> parameters, Token openParen, 
Token closeParen) throws ParseException {
+            // At the moment all built-ins of this kind requires 1 parameter.
             if (parameters.size() != 1) {
                 throw newArgumentCountException("requires exactly 1", 
openParen, closeParen);
             }
@@ -1003,10 +1004,6 @@ class BuiltInsForSequences {
                 // We can't do this with other kind of expressions, as they 
need to be evaluated on runtime:
                 precreatedElementTransformer = new 
LocalLambdaElementTransformer(localLambdaExp);
             }
-
-            if (target instanceof IntermediateStreamOperationLikeBuiltIn) {
-                ((IntermediateStreamOperationLikeBuiltIn) 
target).setLazyProcessingAllowed(true);
-            }
         }
 
         @Override
@@ -1014,12 +1011,17 @@ class BuiltInsForSequences {
             return true;
         }
 
-        boolean isLazyProcessingAllowed() {
-            return lazyProcessingAllowed;
+        @Override
+        protected boolean isLazilyGeneratedSequenceModelTargetSupported() {
+            return true;
+        }
+
+        final boolean isLazyResultGenerationAllowed() {
+            return lazyResultGenerationAllowed;
         }
 
         /**
-         * Used to allow processing of the collection or sequence elements on 
an as-needed basis, similarly as
+         * Used to allow generating the result collection or sequence elements 
on an as-needed basis, similarly as
          * Java 8 Stream intermediate operations do it. This is initially 
{@code false}. The containing expression or
          * directive sets it to {@code true} if it can ensure that:
          * <ul>
@@ -1030,8 +1032,8 @@ class BuiltInsForSequences {
          *       built-in was called. This is required as lambda expression 
are {@link LocalLambdaExpression}-s.
          * </ul>
          */
-        void setLazyProcessingAllowed(boolean lazyProcessingAllowed) {
-            this.lazyProcessingAllowed = lazyProcessingAllowed;
+        void setLazyResultGenerationAllowed(boolean 
lazyResultGenerationAllowed) {
+            this.lazyResultGenerationAllowed = lazyResultGenerationAllowed;
         }
 
         protected List<Expression> getArgumentsAsList() {
@@ -1084,10 +1086,12 @@ class BuiltInsForSequences {
         private TemplateModelIterator getTemplateModelIterator(Environment 
env, TemplateModel model) throws TemplateModelException,
                 NonSequenceOrCollectionException, InvalidReferenceException {
             if (model instanceof TemplateCollectionModel) {
-                return ((TemplateCollectionModel) model).iterator();
+                return isLazyResultGenerationAllowed()
+                        ? new 
LazyCollectionTemplateModelIterator((TemplateCollectionModel) model)
+                        : ((TemplateCollectionModel) model).iterator();
             } else if (model instanceof TemplateSequenceModel) {
-                return new SequenceIterator((TemplateSequenceModel) model);
-            } else if (model instanceof TemplateModelIterator) { // For a 
stream mode LHO
+                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);
@@ -1095,8 +1099,10 @@ class BuiltInsForSequences {
         }
 
         /**
-         * @param lhoIterator Use this to iterate through the items
-         * @param lho Maybe needed for operations specific to the built-in, 
like getting the size
+         * @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 elementTransformer The argument to the built-in (typically a 
lambda expression)
          *
          * @return {@link TemplateSequenceModel} or {@link 
TemplateCollectionModel} or {@link TemplateModelIterator}.
          */
@@ -1104,10 +1110,15 @@ class BuiltInsForSequences {
                 TemplateModelIterator lhoIterator, TemplateModel lho, 
ElementTransformer elementTransformer,
                 Environment env) throws TemplateException;
 
+        /**
+         * Wraps the built-in argument that specifies how to transform the 
elements of the sequence, to hide the
+         * complexity of doing that.
+         */
         interface ElementTransformer {
             TemplateModel transformElement(TemplateModel element, Environment 
env) throws TemplateException;
         }
 
+        /** {@link ElementTransformer} that wraps a local lambda expression. */
         private static class LocalLambdaElementTransformer implements 
ElementTransformer {
             private final LocalLambdaExpression elementTransformerExp;
 
@@ -1120,6 +1131,7 @@ class BuiltInsForSequences {
             }
         }
 
+        /** {@link ElementTransformer} that wraps a (Java) method call. */
         private static class MethodElementTransformer implements 
ElementTransformer {
             private final TemplateMethodModel elementTransformer;
 
@@ -1134,6 +1146,7 @@ class BuiltInsForSequences {
             }
         }
 
+        /** {@link ElementTransformer} that wraps a call to an FTL function 
(things defined with {@code #function}). */
         private static class FunctionElementTransformer implements 
ElementTransformer {
             private final Macro templateTransformer;
             private final Expression elementTransformerExp;
@@ -1145,6 +1158,8 @@ class BuiltInsForSequences {
 
             public TemplateModel transformElement(TemplateModel element, 
Environment env) throws
                     TemplateException {
+                // #function-s were originally designed to be called from 
templates directly, so they expect an
+                // Expression as argument. So we have to create a fake one.
                 ExpressionWithFixedResult functionArgExp = new 
ExpressionWithFixedResult(
                         element, elementTransformerExp);
                 return env.invokeFunction(env, templateTransformer,
@@ -1161,7 +1176,7 @@ class BuiltInsForSequences {
                 final TemplateModelIterator lhoIterator, final TemplateModel 
lho,
                 final ElementTransformer elementTransformer,
                 final Environment env) throws TemplateException {
-            if (!isLazyProcessingAllowed()) {
+            if (!isLazyResultGenerationAllowed()) {
                 List<TemplateModel> resultList = new 
ArrayList<TemplateModel>();
                 while (lhoIterator.hasNext()) {
                     TemplateModel element = lhoIterator.next();
@@ -1171,7 +1186,7 @@ class BuiltInsForSequences {
                 }
                 return new TemplateModelListSequence(resultList);
             } else {
-                return new SingleIterationCollectionModel(
+                return new LazilyGeneratedSequenceModel(
                         new TemplateModelIterator() {
                             boolean prefetchDone;
                             TemplateModel prefetchedElement;
@@ -1223,17 +1238,17 @@ class BuiltInsForSequences {
             }
         }
 
-        private boolean elementMatches(TemplateModel element, 
ElementTransformer elementTransformer, Environment env) throws
-                TemplateException {
+        private boolean elementMatches(TemplateModel element, 
ElementTransformer elementTransformer, Environment env)
+                throws TemplateException {
             TemplateModel transformedElement = 
elementTransformer.transformElement(element, env);
             if (!(transformedElement instanceof TemplateBooleanModel)) {
                 if (transformedElement == null) {
                     throw new 
_TemplateModelException(getElementTransformerExp(), env,
-                            "The element transformer function has returned no 
return value (has returned null) " +
-                            "instead of a boolean.");
+                            "The filter expression has returned no value (has 
returned null), " +
+                            "rather than a boolean.");
                 }
                 throw new _TemplateModelException(getElementTransformerExp(), 
env,
-                        "The element transformer function had to return a 
boolean value, but it has returned ",
+                        "The filter expression had to return a boolean value, 
but it returned ",
                         new _DelayedAOrAn(new 
_DelayedFTLTypeDescription(transformedElement)),
                         " instead.");
             }
@@ -1247,19 +1262,17 @@ class BuiltInsForSequences {
         protected TemplateModel calculateResult(
                 final TemplateModelIterator lhoIterator, TemplateModel lho, 
final ElementTransformer elementTransformer,
                 final Environment env) throws TemplateException {
-            if (!isLazyProcessingAllowed()) {
+            if (!isLazyResultGenerationAllowed()) {
                 List<TemplateModel> resultList = new 
ArrayList<TemplateModel>();
                 while (lhoIterator.hasNext()) {
-                    resultList.add(fetchAndTransformNextElement(lhoIterator, 
elementTransformer, env));
+                    resultList.add(fetchAndMapNextElement(lhoIterator, 
elementTransformer, env));
                 }
                 return new TemplateModelListSequence(resultList);
             } else {
-                return new SingleIterationCollectionModel(
-                        new TemplateModelIterator() {
-
+                TemplateModelIterator mappedLhoIterator = new 
TemplateModelIterator() {
                     public TemplateModel next() throws TemplateModelException {
                         try {
-                            return fetchAndTransformNextElement(lhoIterator, 
elementTransformer, env);
+                            return fetchAndMapNextElement(lhoIterator, 
elementTransformer, env);
                         } catch (TemplateException e) {
                             throw new _TemplateModelException(e, env, "Failed 
to transform element");
                         }
@@ -1268,17 +1281,25 @@ class BuiltInsForSequences {
                     public boolean hasNext() throws TemplateModelException {
                         return lhoIterator.hasNext();
                     }
-                });
+                };
+                if (lho instanceof TemplateCollectionModelEx) { // Preferred 
branch, as TempCollModEx has isEmpty() too
+                    return new 
SameSizeCollLazilyGeneratedSequenceModel(mappedLhoIterator,
+                            (TemplateCollectionModelEx) lho);
+                } else if (lho instanceof TemplateSequenceModel) {
+                    return new 
SameSizeSeqLazilyGeneratedSequenceModel(mappedLhoIterator, 
(TemplateSequenceModel) lho);
+                } else {
+                    return new LazilyGeneratedSequenceModel(mappedLhoIterator);
+                }
             }
         }
 
-        private TemplateModel fetchAndTransformNextElement(
+        private TemplateModel fetchAndMapNextElement(
                 TemplateModelIterator lhoIterator, ElementTransformer 
elementTransformer, Environment env)
                 throws TemplateException {
             TemplateModel transformedElement = 
elementTransformer.transformElement(lhoIterator.next(), env);
             if (transformedElement == null) {
                 throw new _TemplateModelException(getElementTransformerExp(), 
env,
-                        "The element transformer function has returned no 
return value (has returned null).");
+                        "The element mapper function has returned no return 
value (has returned null).");
             }
             return transformedElement;
         }
diff --git a/src/main/java/freemarker/core/Environment.java 
b/src/main/java/freemarker/core/Environment.java
index eff4c91..2bacc33 100644
--- a/src/main/java/freemarker/core/Environment.java
+++ b/src/main/java/freemarker/core/Environment.java
@@ -684,8 +684,6 @@ public final class Environment extends Configurable {
 
     /**
      * Evaluate expression with shadowing a single variable with a new local 
variable.
-     *
-     * @since 2.3.29
      */
     TemplateModel evaluateWithNewLocal(Expression exp, String lambdaArgName, 
TemplateModel lamdaArgValue)
             throws TemplateException {
diff --git a/src/main/java/freemarker/core/IteratorBlock.java 
b/src/main/java/freemarker/core/IteratorBlock.java
index 59e3c35..a51401a 100644
--- a/src/main/java/freemarker/core/IteratorBlock.java
+++ b/src/main/java/freemarker/core/IteratorBlock.java
@@ -84,7 +84,7 @@ final class IteratorBlock extends TemplateElement {
         this.forEach = forEach;
 
         if (listedExp instanceof 
BuiltInsForSequences.IntermediateStreamOperationLikeBuiltIn) {
-            ((BuiltInsForSequences.IntermediateStreamOperationLikeBuiltIn) 
listedExp).setLazyProcessingAllowed(true);
+            ((BuiltInsForSequences.IntermediateStreamOperationLikeBuiltIn) 
listedExp).setLazyResultGenerationAllowed(true);
             fetchElementsOutsideLoopVarContext = true;
         } else {
             fetchElementsOutsideLoopVarContext = false;
@@ -307,7 +307,7 @@ final class IteratorBlock extends TemplateElement {
                         openedIterator = null;
                     } else {
                         // We must reuse this later, because 
TemplateCollectionModel-s that wrap an Iterator only
-                        // allow one iterator() call. (Also those returned by 
?filter, etc. with lazy processing on.)
+                        // allow one iterator() call. (Also those returned by 
?filter, etc. with lazy result generation.)
                         openedIterator = iterModel;
                         // Note: Loop variables will only become visible 
inside #items
                         env.visit(childBuffer);
diff --git a/src/main/java/freemarker/core/LazilyGeneratedSequenceModel.java 
b/src/main/java/freemarker/core/LazilyGeneratedSequenceModel.java
new file mode 100644
index 0000000..a0737f1
--- /dev/null
+++ b/src/main/java/freemarker/core/LazilyGeneratedSequenceModel.java
@@ -0,0 +1,35 @@
+/*
+ * 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.ext.beans.CollectionModel;
+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.
+ */
+class LazilyGeneratedSequenceModel extends SingleIterationCollectionModel {
+    public LazilyGeneratedSequenceModel(TemplateModelIterator iterator) {
+        super(iterator);
+    }
+}
diff --git 
a/src/main/java/freemarker/core/LazyCollectionTemplateModelIterator.java 
b/src/main/java/freemarker/core/LazyCollectionTemplateModelIterator.java
new file mode 100644
index 0000000..aa55c26
--- /dev/null
+++ b/src/main/java/freemarker/core/LazyCollectionTemplateModelIterator.java
@@ -0,0 +1,54 @@
+/*
+ * 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.TemplateModel;
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateModelIterator;
+
+/**
+ * Delays creating the {@link TemplateModelIterator} of the wrapped model 
until it's actually needed.
+ */
+class LazyCollectionTemplateModelIterator implements TemplateModelIterator {
+
+    private final TemplateCollectionModel templateCollectionModel;
+    private TemplateModelIterator iterator;
+
+    public LazyCollectionTemplateModelIterator(TemplateCollectionModel 
templateCollectionModel) {
+        this.templateCollectionModel = templateCollectionModel;
+    }
+
+    public TemplateModel next() throws TemplateModelException {
+        ensureIteratorInitialized();
+        return iterator.next();
+    }
+
+    public boolean hasNext() throws TemplateModelException {
+        ensureIteratorInitialized();
+        return iterator.hasNext();
+    }
+
+    private void ensureIteratorInitialized() throws TemplateModelException {
+        if (iterator == null) {
+            iterator = templateCollectionModel.iterator();
+        }
+    }
+}
diff --git a/src/main/java/freemarker/core/LazySequenceIterator.java 
b/src/main/java/freemarker/core/LazySequenceIterator.java
new file mode 100644
index 0000000..8bcf78d
--- /dev/null
+++ b/src/main/java/freemarker/core/LazySequenceIterator.java
@@ -0,0 +1,55 @@
+/*
+ * 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.TemplateModel;
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateModelIterator;
+import freemarker.template.TemplateSequenceModel;
+
+/**
+ * {@link TemplateModelIterator} that wraps a sequence, delaying calling any 
of its methods until it's actually needed.
+ *
+ * @since 2.3.29
+ */
+class LazySequenceIterator implements TemplateModelIterator {
+    private final TemplateSequenceModel sequence;
+    private Integer size;
+    private int index = 0;
+
+    LazySequenceIterator(TemplateSequenceModel sequence) throws 
TemplateModelException {
+        this.sequence = sequence;
+
+    }
+    public TemplateModel next() throws TemplateModelException {
+        return sequence.get(index++);
+    }
+
+    public boolean hasNext() {
+        if (size == null) {
+            try {
+                size = sequence.size();
+            } catch (TemplateModelException e) {
+                throw new RuntimeException("Error when getting sequence size", 
e);
+            }
+        }
+        return index < size;
+    }
+}
diff --git 
a/src/main/java/freemarker/core/SameSizeLazilyGeneratedSequenceModel.java 
b/src/main/java/freemarker/core/SameSizeLazilyGeneratedSequenceModel.java
new file mode 100644
index 0000000..a0f4225
--- /dev/null
+++ b/src/main/java/freemarker/core/SameSizeLazilyGeneratedSequenceModel.java
@@ -0,0 +1,49 @@
+/*
+ * 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.TemplateCollectionModelEx;
+import freemarker.template.TemplateModelException;
+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
+ * source, if the source can also give back an element count.
+ */
+class SameSizeCollLazilyGeneratedSequenceModel extends 
LazilyGeneratedSequenceModel
+        implements TemplateCollectionModelEx {
+    private final TemplateCollectionModelEx sizeSourceCollEx;
+
+    public SameSizeCollLazilyGeneratedSequenceModel(
+            TemplateModelIterator iterator, TemplateCollectionModelEx 
sizeSourceCollEx) {
+        super(iterator);
+        NullArgumentException.check(sizeSourceCollEx);
+        this.sizeSourceCollEx = sizeSourceCollEx;
+    }
+
+    public int size() throws TemplateModelException {
+        return sizeSourceCollEx.size();
+    }
+
+    public boolean isEmpty() throws TemplateModelException {
+        return sizeSourceCollEx.isEmpty();
+    }
+}
diff --git 
a/src/main/java/freemarker/core/SameSizeSeqLazilyGeneratedSequenceModel.java 
b/src/main/java/freemarker/core/SameSizeSeqLazilyGeneratedSequenceModel.java
new file mode 100644
index 0000000..d6d7b5f
--- /dev/null
+++ b/src/main/java/freemarker/core/SameSizeSeqLazilyGeneratedSequenceModel.java
@@ -0,0 +1,52 @@
+/*
+ * 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.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
+ * source, if the source can also give back an element count.
+ *
+ * @since 2.3.29
+ */
+class SameSizeSeqLazilyGeneratedSequenceModel extends 
LazilyGeneratedSequenceModel
+        implements TemplateCollectionModelEx {
+    private final TemplateSequenceModel sizeSourceSeq;
+
+    public SameSizeSeqLazilyGeneratedSequenceModel(
+            TemplateModelIterator iterator, TemplateSequenceModel 
sizeSourceSeq) {
+        super(iterator);
+        NullArgumentException.check(sizeSourceSeq);
+        this.sizeSourceSeq = sizeSourceSeq;
+    }
+
+    public int size() throws TemplateModelException {
+        return sizeSourceSeq.size();
+    }
+
+    public boolean isEmpty() throws TemplateModelException {
+        return sizeSourceSeq.size() == 0;
+    }
+}
diff --git 
a/src/test/java/freemarker/core/LazilyGeneratedSeqTargetSupportInBuiltinsTest.java
 
b/src/test/java/freemarker/core/LazilyGeneratedSeqTargetSupportInBuiltinsTest.java
new file mode 100644
index 0000000..37cefa8
--- /dev/null
+++ 
b/src/test/java/freemarker/core/LazilyGeneratedSeqTargetSupportInBuiltinsTest.java
@@ -0,0 +1,174 @@
+/*
+ * 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 java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+
+import org.junit.Test;
+
+import freemarker.template.Configuration;
+import freemarker.template.SimpleNumber;
+import freemarker.template.TemplateCollectionModel;
+import freemarker.template.TemplateCollectionModelEx;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateModelIterator;
+import freemarker.template.TemplateSequenceModel;
+import freemarker.test.TemplateTest;
+
+/**
+ * Tests built-ins that are support getting {@link 
LazilyGeneratedSequenceModel} as their LHO input.
+ */
+public class LazilyGeneratedSeqTargetSupportInBuiltinsTest extends 
TemplateTest {
+
+    @Test
+    public void sizeTest() throws Exception {
+        assertOutput("${seq?size}",
+                "[size]3");
+        assertOutput("${collEx?size}",
+                "[size]3");
+        assertErrorContains("${coll?size}",
+                "sequence", "extended collection");
+
+        assertOutput("${seq?map(x -> x * 10)?size}",
+                "[size]3");
+        assertOutput("${collEx?map(x -> x * 10)?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}",
+                
"[iterator][hasNext][next][hasNext][next][hasNext][next][hasNext]2");
+    }
+
+    @Test
+    public void firstTest() throws Exception {
+        assertOutput("${coll?first}",
+                "[iterator][hasNext][next]1");
+        assertOutput("${coll?filter(x -> x % 2 == 0)?first}",
+                "[iterator][hasNext][next][hasNext][next]2");
+    }
+
+    @Test
+    public void seqIndexOfTest() throws Exception {
+        assertOutput("${coll?seqIndexOf(2)}",
+                "[iterator][hasNext][next][hasNext][next]1");
+        assertOutput("${coll?filter(x -> x % 2 == 0)?seqIndexOf(2)}",
+                "[iterator][hasNext][next][hasNext][next]0");
+    }
+
+    @Test
+    public void filterTest() throws Exception {
+        assertOutput("${coll?filter(x -> x % 2 == 0)?filter(x -> true)?first}",
+                "[iterator][hasNext][next][hasNext][next]2");
+    }
+
+    @Test
+    public void listTest() throws Exception {
+        // Note: #list has to fetch elements up to 4 to know if it?hasNext
+        assertOutput("<#list collLong?filter(x -> x % 2 == 0) as it>${it} 
${it?hasNext?c}<#break></#list>",
+                
"[iterator][hasNext][next][hasNext][next][hasNext][next][hasNext][next]2 true");
+    }
+
+    @Override
+    protected Configuration createConfiguration() throws Exception {
+        Configuration cfg = super.createConfiguration();
+        cfg.setSharedVariable("seq", new MonitoredTemplateSequenceModel(1, 2, 
3));
+        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;
+
+        public ListContainingTemplateModel(Number... elements) {
+            this.elements = new ArrayList<Number>(Arrays.asList(elements));
+        }
+
+        public int size() {
+            logCall("size");
+            return elements.size();
+        }
+
+        protected void logCall(String callDesc) {
+            try {
+                Environment.getCurrentEnvironment().getOut().write("[" + 
callDesc + "]");
+            } catch (IOException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+    }
+
+    public static class MonitoredTemplateSequenceModel extends 
ListContainingTemplateModel
+            implements TemplateSequenceModel {
+        public MonitoredTemplateSequenceModel(Number... elements) {
+            super(elements);
+        }
+
+        public TemplateModel get(int index) throws TemplateModelException {
+            logCall("get " + index);
+            return new SimpleNumber(elements.get(index));
+        }
+
+    }
+
+    public static class MonitoredTemplateCollectionModel extends 
ListContainingTemplateModel
+            implements TemplateCollectionModel {
+        public MonitoredTemplateCollectionModel(Number... elements) {
+            super(elements);
+        }
+
+        public TemplateModelIterator iterator() throws TemplateModelException {
+            logCall("iterator");
+            final Iterator<Number> iterator = elements.iterator();
+            return new TemplateModelIterator() {
+                public TemplateModel next() throws TemplateModelException {
+                    logCall("next");
+                    return new SimpleNumber(iterator.next());
+                }
+
+                public boolean hasNext() throws TemplateModelException {
+                    logCall("hasNext");
+                    return iterator.hasNext();
+                }
+            };
+        }
+    }
+
+    public static class MonitoredTemplateCollectionModelEx extends 
MonitoredTemplateCollectionModel
+            implements TemplateCollectionModelEx {
+        public MonitoredTemplateCollectionModelEx(Number... elements) {
+            super(elements);
+        }
+
+        public boolean isEmpty() throws TemplateModelException {
+            logCall("isEmpty");
+            return elements.isEmpty();
+        }
+    }
+
+}
diff --git a/src/test/java/freemarker/core/ListWithStreamLikeBuiltinsTest.java 
b/src/test/java/freemarker/core/ListWithStreamLikeBuiltinsTest.java
index 1438304..f025e9a 100644
--- a/src/test/java/freemarker/core/ListWithStreamLikeBuiltinsTest.java
+++ b/src/test/java/freemarker/core/ListWithStreamLikeBuiltinsTest.java
@@ -36,7 +36,7 @@ public class ListWithStreamLikeBuiltinsTest extends 
TemplateTest {
 
     @Test
     public void testLambdaScope() throws Exception {
-        // The loop variables aren't visible during the lazy processing done 
for the elements
+        // Loop variables aren't visible during the lazy result generation:
         assertOutput("<#list (1..3)?map(p -> p * 10 + it!'-') as 
it>${it}<#sep>, </#list>",
                 "10-, 20-, 30-");
         assertOutput("<#list (1..3)?map(p -> p * 10 + it_has_next!'-') as 
it>${it}<#sep>, </#list>",

Reply via email to