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 7e4ab15adb09ea6347ee5052b6b027c69cbdeafe
Author: ddekany <[email protected]>
AuthorDate: Sun Feb 17 15:39:38 2019 +0100

    Added "local lambdas" to parser, which look like Java lambda-s, but are 
only allowed as parameters in certain built-ins, as they can't work outside the 
variable scope where they were created. To added new built-ins, ?filter(...) 
and ?map(...) to try the concept. These can get a "local lambda", or an #ftl 
function, or a method as parameter. These built-ins support the lazy processing 
of elements when they are the child of an AST node that explicitly enables that 
(for now only #list and [...]
---
 src/main/java/freemarker/core/BuiltIn.java         |  14 +-
 .../core/BuiltInWithParseTimeParameters.java       |  21 +-
 .../java/freemarker/core/BuiltInsForSequences.java | 345 ++++++++++++++++++++-
 src/main/java/freemarker/core/Environment.java     |  38 +++
 .../freemarker/core/ExpressionWithFixedResult.java |  73 +++++
 src/main/java/freemarker/core/IteratorBlock.java   |  41 +--
 .../freemarker/core/LocalLambdaExpression.java     | 152 +++++++++
 src/main/java/freemarker/core/MethodCall.java      |   2 +-
 .../java/freemarker/core/NonMethodException.java   |   7 +-
 .../core/SingleIterationCollectionModel.java       |  51 +++
 src/main/javacc/FTL.jj                             | 127 +++++++-
 src/test/java/freemarker/core/FilterBiTest.java    | 142 +++++++++
 src/test/java/freemarker/core/MapBiTest.java       | 258 +++++++++++++++
 13 files changed, 1234 insertions(+), 37 deletions(-)

diff --git a/src/main/java/freemarker/core/BuiltIn.java 
b/src/main/java/freemarker/core/BuiltIn.java
index 7d063ab..450dd6d 100644
--- a/src/main/java/freemarker/core/BuiltIn.java
+++ b/src/main/java/freemarker/core/BuiltIn.java
@@ -84,7 +84,7 @@ abstract class BuiltIn extends Expression implements 
Cloneable {
 
     static final Set<String> CAMEL_CASE_NAMES = new TreeSet<String>();
     static final Set<String> SNAKE_CASE_NAMES = new TreeSet<String>();
-    static final int NUMBER_OF_BIS = 279;
+    static final int NUMBER_OF_BIS = 281;
     static final HashMap<String, BuiltIn> BUILT_INS_BY_NAME = new 
HashMap(NUMBER_OF_BIS * 3 / 2 + 1, 1f);
 
     static {
@@ -115,6 +115,7 @@ abstract class BuiltIn extends Expression implements 
Cloneable {
         putBI("esc", new escBI());
         putBI("eval", new evalBI());
         putBI("exists", new BuiltInsForExistenceHandling.existsBI());
+        putBI("filter", new BuiltInsForSequences.filterBI());
         putBI("first", new firstBI());
         putBI("float", new floatBI());
         putBI("floor", new floorBI());
@@ -238,6 +239,7 @@ abstract class BuiltIn extends Expression implements 
Cloneable {
         putBI("long", new longBI());
         putBI("lower_abc", "lowerAbc", new BuiltInsForNumbers.lower_abcBI());
         putBI("lower_case", "lowerCase", new 
BuiltInsForStringsBasic.lower_caseBI());
+        putBI("map", new BuiltInsForSequences.mapBI());
         putBI("namespace", new BuiltInsForMultipleTypes.namespaceBI());
         putBI("new", new NewBI());
         putBI("markup_string", "markupString", new markup_stringBI());
@@ -385,9 +387,19 @@ abstract class BuiltIn extends Expression implements 
Cloneable {
         }
         bi.key = key;
         bi.target = target;
+        if (bi.isSingleIterationCollectionTargetSupported()) {
+            if (target instanceof 
BuiltInsForSequences.IntermediateStreamOperationLikeBuiltIn) {
+                ((BuiltInsForSequences.IntermediateStreamOperationLikeBuiltIn) 
target)
+                        .setLazyProcessingAllowed(true);
+            }
+        }
         return bi;
     }
 
+    protected boolean isSingleIterationCollectionTargetSupported() {
+        return false;
+    }
+
     @Override
     public String getCanonicalForm() {
         return target.getCanonicalForm() + "?" + key;
diff --git a/src/main/java/freemarker/core/BuiltInWithParseTimeParameters.java 
b/src/main/java/freemarker/core/BuiltInWithParseTimeParameters.java
index f17c204..383506f 100644
--- a/src/main/java/freemarker/core/BuiltInWithParseTimeParameters.java
+++ b/src/main/java/freemarker/core/BuiltInWithParseTimeParameters.java
@@ -85,13 +85,25 @@ abstract class BuiltInWithParseTimeParameters extends 
SpecialBuiltIn {
         }
     }
 
-    protected ParseException newArgumentCountException(String ordinalityDesc, 
Token openParen, Token closeParen) {
+    protected final ParseException newArgumentCountException(String 
ordinalityDesc, Token openParen, Token closeParen) {
         return new ParseException(
                 "?" + key + "(...) " + ordinalityDesc + " parameters", 
this.getTemplate(),
                 openParen.beginLine, openParen.beginColumn,
                 closeParen.endLine, closeParen.endColumn);
     }
 
+    protected final void checkLocalLambdaParamCount(LocalLambdaExpression 
localLambdaExp, int expectedParamCount)
+            throws ParseException {
+        int actualParamCount = 
localLambdaExp.getLambdaParameterList().getParameters().size();
+        if (actualParamCount != expectedParamCount) {
+            throw new ParseException(
+                    "?" + key + "(...) parameter lambda expression must 
declare exactly " + expectedParamCount + " "
+                            + "parameter" + (expectedParamCount > 1 ? "s" : 
"") + ", but it declared "
+                            + actualParamCount + ".",
+                    localLambdaExp);
+        }
+    }
+
     @Override
     protected Expression deepCloneWithIdentifierReplaced_inner(
             String replacedIdentifier, Expression replacement, 
ReplacemenetState replacementState) {
@@ -108,5 +120,12 @@ abstract class BuiltInWithParseTimeParameters extends 
SpecialBuiltIn {
     
     protected abstract void cloneArguments(Expression clone, String 
replacedIdentifier,
             Expression replacement, ReplacemenetState replacementState);
+
+    /**
+     * If parameter expressions can be {@link LocalLambdaExpression}-s.
+     */
+    protected boolean isLocalLambdaParameterSupported() {
+        return false;
+    }
     
 }
diff --git a/src/main/java/freemarker/core/BuiltInsForSequences.java 
b/src/main/java/freemarker/core/BuiltInsForSequences.java
index 9bc8d95..0bb2bd9 100644
--- a/src/main/java/freemarker/core/BuiltInsForSequences.java
+++ b/src/main/java/freemarker/core/BuiltInsForSequences.java
@@ -37,6 +37,7 @@ import freemarker.template.TemplateCollectionModelEx;
 import freemarker.template.TemplateDateModel;
 import freemarker.template.TemplateException;
 import freemarker.template.TemplateHashModel;
+import freemarker.template.TemplateMethodModel;
 import freemarker.template.TemplateMethodModelEx;
 import freemarker.template.TemplateModel;
 import freemarker.template.TemplateModelException;
@@ -141,7 +142,12 @@ class BuiltInsForSequences {
     }
     
     static class firstBI extends BuiltIn {
-        
+
+        @Override
+        protected boolean isSingleIterationCollectionTargetSupported() {
+            return true;
+        }
+
         @Override
         TemplateModel _eval(Environment env)
                 throws TemplateException {
@@ -177,7 +183,12 @@ class BuiltInsForSequences {
     }
 
     static class joinBI extends BuiltIn {
-        
+
+        @Override
+        protected boolean isSingleIterationCollectionTargetSupported() {
+            return true;
+        }
+
         private class BIMethodForCollection implements TemplateMethodModelEx {
             
             private final Environment env;
@@ -246,7 +257,7 @@ class BuiltInsForSequences {
                 throw new NonSequenceOrCollectionException(target, model, env);
             }
         }
-   
+
     }
 
     static class lastBI extends BuiltInForSequence {
@@ -289,6 +300,12 @@ class BuiltInsForSequences {
     }
 
     static class seq_containsBI extends BuiltIn {
+
+        @Override
+        protected boolean isSingleIterationCollectionTargetSupported() {
+            return true;
+        }
+
         private class BIMethodForCollection implements TemplateMethodModelEx {
             private TemplateCollectionModel m_coll;
             private Environment m_env;
@@ -355,7 +372,12 @@ class BuiltInsForSequences {
     }
     
     static class seq_index_ofBI extends BuiltIn {
-        
+
+        @Override
+        protected boolean isSingleIterationCollectionTargetSupported() {
+            return true;
+        }
+
         private class BIMethod implements TemplateMethodModelEx {
             
             protected final TemplateSequenceModel m_seq;
@@ -893,6 +915,11 @@ class BuiltInsForSequences {
         }
 
         @Override
+        protected boolean isSingleIterationCollectionTargetSupported() {
+            return true;
+        }
+
+        @Override
         TemplateModel _eval(Environment env)
                 throws TemplateException {
             TemplateModel model = target.eval(env);
@@ -951,8 +978,314 @@ 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.
+     */
+    static abstract class IntermediateStreamOperationLikeBuiltIn extends 
BuiltInWithParseTimeParameters {
+
+        private Expression elementTransformerExp;
+        private ElementTransformer precreatedElementTransformer;
+        private boolean lazyProcessingAllowed;
+
+        @Override
+        void bindToParameters(List<Expression> parameters, Token openParen, 
Token closeParen) throws ParseException {
+            if (parameters.size() != 1) {
+                throw newArgumentCountException("requires exactly 1", 
openParen, closeParen);
+            }
+            this.elementTransformerExp = parameters.get(0);
+            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:
+                precreatedElementTransformer = new 
LocalLambdaElementTransformer(localLambdaExp);
+            }
+
+            if (target instanceof IntermediateStreamOperationLikeBuiltIn) {
+                ((IntermediateStreamOperationLikeBuiltIn) 
target).setLazyProcessingAllowed(true);
+            }
+        }
+
+        @Override
+        protected boolean isLocalLambdaParameterSupported() {
+            return true;
+        }
+
+        boolean isLazyProcessingAllowed() {
+            return lazyProcessingAllowed;
+        }
+
+        /**
+         * Used to allow processing of the 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>
+         *   <li>The returned {@link TemplateCollectionModel} is traversed 
only once, more specifically,
+         *       {@link TemplateCollectionModel#iterator()} is called only 
once.
+         *   <li>When the methods of the collection or iterator are called, 
the context provided by
+         *       the {@link Environment} (such as the local context stack) is 
similar to the context from where the
+         *       built-in was called. This is required as lambda expression 
are {@link LocalLambdaExpression}-s.
+         * </ul>
+         */
+        void setLazyProcessingAllowed(boolean lazyProcessingAllowed) {
+            this.lazyProcessingAllowed = lazyProcessingAllowed;
+        }
+
+        protected List<Expression> getArgumentsAsList() {
+            return Collections.singletonList(elementTransformerExp);
+        }
+
+        protected int getArgumentsCount() {
+            return 1;
+        }
+
+        protected Expression getArgumentParameterValue(int argIdx) {
+            if (argIdx != 0) {
+                throw new IndexOutOfBoundsException();
+            }
+            return elementTransformerExp;
+        }
+
+        protected Expression getElementTransformerExp() {
+            return elementTransformerExp;
+        }
+
+        protected void cloneArguments(
+                Expression clone, String replacedIdentifier, Expression 
replacement, ReplacemenetState replacementState) {
+            ((IntermediateStreamOperationLikeBuiltIn) 
clone).elementTransformerExp
+                    = elementTransformerExp.deepCloneWithIdentifierReplaced(
+                            replacedIdentifier, replacement, replacementState);
+        }
+
+        TemplateModel _eval(Environment env) throws TemplateException {
+            TemplateModel lho = target.eval(env);
+            TemplateModelIterator lhoIterator = getTemplateModelIterator(env, 
lho);
+            return calculateResult(lhoIterator, lho, 
evalElementTransformerExp(env), env);
+        }
+
+        private ElementTransformer evalElementTransformerExp(Environment env) 
throws TemplateException {
+            if (precreatedElementTransformer != null) {
+                return precreatedElementTransformer;
+            }
+
+            TemplateModel elementTransformerModel = 
elementTransformerExp.eval(env);
+            if (elementTransformerModel instanceof TemplateMethodModel) {
+                return new MethodElementTransformer((TemplateMethodModel) 
elementTransformerModel);
+            } else if (elementTransformerModel instanceof Macro) {
+                return new FunctionElementTransformer((Macro) 
elementTransformerModel, elementTransformerExp);
+            } else {
+                throw new NonMethodException(elementTransformerExp, 
elementTransformerModel, true, true, null, env);
+            }
+        }
+
+        private TemplateModelIterator getTemplateModelIterator(Environment 
env, TemplateModel model) throws TemplateModelException,
+                NonSequenceOrCollectionException, InvalidReferenceException {
+            if (model instanceof TemplateCollectionModel) {
+                return ((TemplateCollectionModel) model).iterator();
+            } else if (model instanceof TemplateSequenceModel) {
+                return new SequenceIterator((TemplateSequenceModel) model);
+            } else if (model instanceof TemplateModelIterator) { // For a 
stream mode LHO
+                return (TemplateModelIterator) model;
+            } else {
+                throw new NonSequenceOrCollectionException(target, model, env);
+            }
+        }
+
+        /**
+         * @param lhoIterator Use this to iterate through the items
+         * @param lho Maybe needed for operations specific to the built-in, 
like getting the size
+         *
+         * @return {@link TemplateSequenceModel} or {@link 
TemplateCollectionModel} or {@link TemplateModelIterator}.
+         */
+        protected abstract TemplateModel calculateResult(
+                TemplateModelIterator lhoIterator, TemplateModel lho, 
ElementTransformer elementTransformer,
+                Environment env) throws TemplateException;
+
+        interface ElementTransformer {
+            TemplateModel transformElement(TemplateModel element, Environment 
env) throws TemplateException;
+        }
+
+        private static class LocalLambdaElementTransformer implements 
ElementTransformer {
+            private final LocalLambdaExpression elementTransformerExp;
+
+            public LocalLambdaElementTransformer(LocalLambdaExpression 
elementTransformerExp) {
+                this.elementTransformerExp = elementTransformerExp;
+            }
+
+            public TemplateModel transformElement(TemplateModel element, 
Environment env) throws TemplateException {
+                return 
elementTransformerExp.invokeLambdaDefinedFunction(element, env);
+            }
+        }
+
+        private static class MethodElementTransformer implements 
ElementTransformer {
+            private final TemplateMethodModel elementTransformer;
+
+            public MethodElementTransformer(TemplateMethodModel 
elementTransformer) {
+                this.elementTransformer = elementTransformer;
+            }
+
+            public TemplateModel transformElement(TemplateModel element, 
Environment env)
+                    throws TemplateModelException {
+                Object result = 
elementTransformer.exec(Collections.singletonList(element));
+                return result instanceof TemplateModel ? (TemplateModel) 
result : env.getObjectWrapper().wrap(result);
+            }
+        }
+
+        private static class FunctionElementTransformer implements 
ElementTransformer {
+            private final Macro templateTransformer;
+            private final Expression elementTransformerExp;
+
+            public FunctionElementTransformer(Macro templateTransformer, 
Expression elementTransformerExp) {
+                this.templateTransformer = templateTransformer;
+                this.elementTransformerExp = elementTransformerExp;
+            }
+
+            public TemplateModel transformElement(TemplateModel element, 
Environment env) throws
+                    TemplateException {
+                ExpressionWithFixedResult functionArgExp = new 
ExpressionWithFixedResult(
+                        element, elementTransformerExp);
+                return env.invokeFunction(env, templateTransformer,
+                        Collections.singletonList(functionArgExp),
+                        elementTransformerExp);
+            }
+        }
+
+    }
+
+    static class filterBI extends IntermediateStreamOperationLikeBuiltIn {
+
+        protected TemplateModel calculateResult(
+                final TemplateModelIterator lhoIterator, final TemplateModel 
lho,
+                final ElementTransformer elementTransformer,
+                final Environment env) throws TemplateException {
+            if (!isLazyProcessingAllowed()) {
+                List<TemplateModel> resultList = new 
ArrayList<TemplateModel>();
+                while (lhoIterator.hasNext()) {
+                    TemplateModel element = lhoIterator.next();
+                    if (elementMatches(element, elementTransformer, env)) {
+                        resultList.add(element);
+                    }
+                }
+                return new TemplateModelListSequence(resultList);
+            } else {
+                return new SingleIterationCollectionModel(
+                        new TemplateModelIterator() {
+                            boolean prefetchDone;
+                            TemplateModel prefetchedElement;
+                            boolean prefetchedEndOfIterator;
+
+                            public TemplateModel next() throws 
TemplateModelException {
+                                ensurePrefetchDone();
+                                if (prefetchedEndOfIterator) {
+                                    throw new IllegalStateException("next() 
was called when hasNext() is false");
+                                }
+                                prefetchDone = false;
+                                return prefetchedElement;
+                            }
+
+                            public boolean hasNext() throws 
TemplateModelException {
+                                ensurePrefetchDone();
+                                return !prefetchedEndOfIterator;
+                            }
+
+                            private void ensurePrefetchDone() throws 
TemplateModelException {
+                                if (prefetchDone) {
+                                    return;
+                                }
+
+                                boolean conclusionReached = false;
+                                do {
+                                    if (lhoIterator.hasNext()) {
+                                        TemplateModel element = 
lhoIterator.next();
+                                        boolean elementMatched;
+                                        try {
+                                            elementMatched = 
elementMatches(element, elementTransformer, env);
+                                        } catch (TemplateException e) {
+                                            throw new 
_TemplateModelException(e, env, "Failed to transform element");
+                                        }
+                                        if (elementMatched) {
+                                            prefetchedElement = element;
+                                            conclusionReached = true;
+                                        }
+                                    } else {
+                                        prefetchedEndOfIterator = true;
+                                        prefetchedElement = null;
+                                        conclusionReached = true;
+                                    }
+                                } while (!conclusionReached);
+                                prefetchDone = true;
+                            }
+                        }
+                );
+            }
+        }
+
+        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.");
+                }
+                throw new _TemplateModelException(getElementTransformerExp(), 
env,
+                        "The element transformer function had to return a 
boolean value, but it has returned ",
+                        new _DelayedAOrAn(new 
_DelayedFTLTypeDescription(transformedElement)),
+                        " instead.");
+            }
+            return ((TemplateBooleanModel) transformedElement).getAsBoolean();
+        }
+
+    }
+
+    static class mapBI extends IntermediateStreamOperationLikeBuiltIn {
+
+        protected TemplateModel calculateResult(
+                final TemplateModelIterator lhoIterator, TemplateModel lho, 
final ElementTransformer elementTransformer,
+                final Environment env) throws TemplateException {
+            if (!isLazyProcessingAllowed()) {
+                List<TemplateModel> resultList = new 
ArrayList<TemplateModel>();
+                while (lhoIterator.hasNext()) {
+                    resultList.add(fetchAndTransformNextElement(lhoIterator, 
elementTransformer, env));
+                }
+                return new TemplateModelListSequence(resultList);
+            } else {
+                return new SingleIterationCollectionModel(
+                        new TemplateModelIterator() {
+
+                    public TemplateModel next() throws TemplateModelException {
+                        try {
+                            return fetchAndTransformNextElement(lhoIterator, 
elementTransformer, env);
+                        } catch (TemplateException e) {
+                            throw new _TemplateModelException(e, env, "Failed 
to transform element");
+                        }
+                    }
+
+                    public boolean hasNext() throws TemplateModelException {
+                        return lhoIterator.hasNext();
+                    }
+                });
+            }
+        }
+
+        private TemplateModel fetchAndTransformNextElement(
+                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).");
+            }
+            return transformedElement;
+        }
+
+    }
+
     // Can't be instantiated
     private BuiltInsForSequences() { }
-    
+
 }
\ No newline at end of file
diff --git a/src/main/java/freemarker/core/Environment.java 
b/src/main/java/freemarker/core/Environment.java
index 376b838..3498a03 100644
--- a/src/main/java/freemarker/core/Environment.java
+++ b/src/main/java/freemarker/core/Environment.java
@@ -31,6 +31,7 @@ import java.text.DecimalFormatSymbols;
 import java.text.NumberFormat;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.IdentityHashMap;
@@ -649,6 +650,43 @@ 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 {
+        pushLocalContext(new LocalContextWithNewLocal(lambdaArgName, 
lamdaArgValue));
+        try {
+            return exp.eval(this);
+        } finally {
+            localContextStack.pop();
+        }
+    }
+
+    /**
+     * Specialization for 1 local variables.
+     */
+    private static class LocalContextWithNewLocal implements LocalContext {
+        private final String lambdaArgName;
+        private final TemplateModel lambdaArgValue;
+
+        public LocalContextWithNewLocal(String lambdaArgName, TemplateModel 
lambdaArgValue) {
+            this.lambdaArgName = lambdaArgName;
+            this.lambdaArgValue = lambdaArgValue;
+        }
+
+        public TemplateModel getLocalVariable(String name) throws 
TemplateModelException {
+            // TODO [lambda] Do not allow fallback (i.e., introduce 
untransparent null-s)
+            return name.equals(lambdaArgName) ? lambdaArgValue : null;
+        }
+
+        public Collection getLocalVariableNames() throws 
TemplateModelException {
+            return Collections.singleton(lambdaArgName);
+        }
+    }
+
+    /**
      * Used for {@code #visit} and {@code #recurse}.
      */
     void invokeNodeHandlerFor(TemplateNodeModel node, TemplateSequenceModel 
namespaces)
diff --git a/src/main/java/freemarker/core/ExpressionWithFixedResult.java 
b/src/main/java/freemarker/core/ExpressionWithFixedResult.java
new file mode 100644
index 0000000..37aee8f
--- /dev/null
+++ b/src/main/java/freemarker/core/ExpressionWithFixedResult.java
@@ -0,0 +1,73 @@
+/*
+ * 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.TemplateException;
+import freemarker.template.TemplateModel;
+
+/**
+ * Mimics an expression (the "source expression"), but returns the predefined 
"fixed result" whenever it's evaluated.
+ */
+class ExpressionWithFixedResult extends Expression {
+    private final TemplateModel fixedResult;
+    private final Expression sourceExpression;
+
+    ExpressionWithFixedResult(TemplateModel fixedResult, Expression 
sourceExpression) {
+        this.fixedResult = fixedResult;
+        this.sourceExpression = sourceExpression;
+    }
+
+    TemplateModel _eval(Environment env) throws TemplateException {
+        return fixedResult;
+    }
+
+    boolean isLiteral() {
+        return sourceExpression.isLiteral();
+    }
+
+    protected Expression deepCloneWithIdentifierReplaced_inner(String 
replacedIdentifier, Expression replacement,
+            ReplacemenetState replacementState) {
+        return new ExpressionWithFixedResult(
+                fixedResult,
+                
sourceExpression.deepCloneWithIdentifierReplaced(replacedIdentifier, 
replacement, replacementState));
+    }
+
+    public String getCanonicalForm() {
+        return sourceExpression.getCanonicalForm();
+    }
+
+    String getNodeTypeSymbol() {
+        return sourceExpression.getNodeTypeSymbol();
+    }
+
+    int getParameterCount() {
+        return sourceExpression.getParameterCount();
+    }
+
+    Object getParameterValue(int idx) {
+        return sourceExpression.getParameterValue(idx);
+    }
+
+    ParameterRole getParameterRole(int idx) {
+        return sourceExpression.getParameterRole(idx);
+    }
+
+
+}
diff --git a/src/main/java/freemarker/core/IteratorBlock.java 
b/src/main/java/freemarker/core/IteratorBlock.java
index bbb6773..e8ed614 100644
--- a/src/main/java/freemarker/core/IteratorBlock.java
+++ b/src/main/java/freemarker/core/IteratorBlock.java
@@ -49,6 +49,7 @@ final class IteratorBlock extends TemplateElement {
     private final String loopVar2Name;
     private final boolean hashListing;
     private final boolean forEach;
+    private final boolean fetchElementsOutsideLoopVarContext;
 
     /**
      * @param listedExp
@@ -82,6 +83,13 @@ final class IteratorBlock extends TemplateElement {
         setChildren(childrenBeforeElse);
         this.hashListing = hashListing;
         this.forEach = forEach;
+
+        if (listedExp instanceof 
BuiltInsForSequences.IntermediateStreamOperationLikeBuiltIn) {
+            ((BuiltInsForSequences.IntermediateStreamOperationLikeBuiltIn) 
listedExp).setLazyProcessingAllowed(true);
+            fetchElementsOutsideLoopVarContext = true;
+        } else {
+            fetchElementsOutsideLoopVarContext = false;
+        }
     }
     
     boolean isHashListing() {
@@ -243,8 +251,7 @@ final class IteratorBlock extends TemplateElement {
         }
 
         void loopForItemsElement(Environment env, TemplateElement[] 
childBuffer, String loopVarName, String loopVar2Name)
-                    throws NonSequenceOrCollectionException, 
TemplateModelException, InvalidReferenceException,
-                    TemplateException, IOException {
+                    throws TemplateException, IOException {
             try {
                 if (alreadyEntered) {
                     throw new _MiscTemplateException(env,
@@ -265,16 +272,14 @@ final class IteratorBlock extends TemplateElement {
          * 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 {
+                throws TemplateException, IOException  {
             return !hashListing
                     ? executedNestedContentForCollOrSeqListing(env, 
childBuffer)
                     : executedNestedContentForHashListing(env, childBuffer);
         }
 
         private boolean executedNestedContentForCollOrSeqListing(Environment 
env, TemplateElement[] childBuffer)
-                throws TemplateModelException, IOException, TemplateException,
-                NonSequenceOrCollectionException, InvalidReferenceException {
+                throws IOException, TemplateException {
             final boolean listNotEmpty;
             if (listedValue instanceof TemplateCollectionModel) {
                 final TemplateCollectionModel collModel = 
(TemplateCollectionModel) listedValue;
@@ -284,22 +289,22 @@ final class IteratorBlock extends TemplateElement {
                 listNotEmpty = iterModel.hasNext();
                 if (listNotEmpty) {
                     if (loopVarName != null) {
-                            listLoop: do {
-                                loopVar = iterModel.next();
-                                hasNext = iterModel.hasNext();
-                                try {
-                                    env.visit(childBuffer);
-                                } catch (BreakOrContinueException br) {
-                                    if (br == 
BreakOrContinueException.BREAK_INSTANCE) {
-                                        break listLoop;
-                                    }
+                        listLoop: do {
+                            loopVar = iterModel.next();
+                            hasNext = iterModel.hasNext();
+                            try {
+                                env.visit(childBuffer);
+                            } catch (BreakOrContinueException br) {
+                                if (br == 
BreakOrContinueException.BREAK_INSTANCE) {
+                                    break listLoop;
                                 }
-                                index++;
-                            } while (hasNext);
+                            }
+                            index++;
+                        } while (hasNext);
                         openedIterator = null;
                     } else {
                         // We must reuse this later, because 
TemplateCollectionModel-s that wrap an Iterator only
-                        // allow one iterator() call.
+                        // allow one iterator() call. (Also those returned by 
?filter, etc. with lazy processing on.)
                         openedIterator = iterModel;
                         env.visit(childBuffer);
                     }
diff --git a/src/main/java/freemarker/core/LocalLambdaExpression.java 
b/src/main/java/freemarker/core/LocalLambdaExpression.java
new file mode 100644
index 0000000..537efb1
--- /dev/null
+++ b/src/main/java/freemarker/core/LocalLambdaExpression.java
@@ -0,0 +1,152 @@
+/*
+ * 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.util.List;
+
+import freemarker.template.TemplateException;
+import freemarker.template.TemplateModel;
+
+/**
+ * A local lambada expression is a lambda expression that creates a function 
that can only be called from the same
+ * context where it was created, and thus it doesn't need closure support. As 
of this writing (2019-02), this is the
+ * only kind of lambda expression supported, as supporting closures would add 
overhead to many basic operations, while
+ * local lambdas are "good enough" for the main use cases in templates (for 
filtering/transforming lists). Also,
+ * closures can be quite confusing when the lambda expression refers to 
variables that are not effectively final,
+ * such as a loop variable. So that's yet another issue to address if we go 
for less restricted lambdas.
+ */
+final class LocalLambdaExpression extends Expression {
+
+    private final LambdaParameterList lho;
+    private final Expression rho;
+
+    LocalLambdaExpression(LambdaParameterList lho, Expression rho) {
+        this.lho = lho;
+        this.rho = rho;
+    }
+
+    @Override
+    public String getCanonicalForm() {
+        return lho.getCanonicalForm() + " -> " + rho.getCanonicalForm();
+    }
+    
+    @Override
+    String getNodeTypeSymbol() {
+        return "->";
+    }
+
+    TemplateModel _eval(Environment env) throws TemplateException {
+        throw new TemplateException("Can't get lambda expression as a value: 
Lambdas currently can only be used on a " +
+                "few special places.",
+                env);
+    }
+
+    /**
+     * Call the function defined by the lambda expression; overload 
specialized for 1 argument, the most common case.
+     */
+    TemplateModel invokeLambdaDefinedFunction(TemplateModel argValue, 
Environment env) throws TemplateException {
+        return env.evaluateWithNewLocal(rho, 
lho.getParameters().get(0).getName(), argValue);
+    }
+
+    @Override
+    boolean isLiteral() {
+        // As we don't support true lambdas, they can't be evaluted in parse 
time.
+        return false;
+    }
+
+    @Override
+    protected Expression deepCloneWithIdentifierReplaced_inner(
+            String replacedIdentifier, Expression replacement, 
ReplacemenetState replacementState) {
+        // TODO [lambda] replacement in lho is illegal; detect it
+       return new LocalLambdaExpression(
+               lho,
+               rho.deepCloneWithIdentifierReplaced(replacedIdentifier, 
replacement, replacementState));
+    }
+
+    @Override
+    int getParameterCount() {
+        return 2;
+    }
+
+    @Override
+    Object getParameterValue(int idx) {
+        // TODO [lambda] should be similar to #function
+        switch (idx) {
+        case 0: return lho;
+        case 1: return rho;
+        default: throw new IndexOutOfBoundsException();
+        }
+    }
+
+    @Override
+    ParameterRole getParameterRole(int idx) {
+        return ParameterRole.forBinaryOperatorOperand(idx);
+    }
+
+    LambdaParameterList getLambdaParameterList() {
+        return lho;
+    }
+
+    /** The left side of the `->`. */
+    static class LambdaParameterList {
+        private final Token openingParenthesis;
+        private final Token closingParenthesis;
+        private final List<Identifier> parameters;
+
+        public LambdaParameterList(Token openingParenthesis, List<Identifier> 
parameters, Token closingParenthesis) {
+            this.openingParenthesis = openingParenthesis;
+            this.closingParenthesis = closingParenthesis;
+            this.parameters = parameters;
+        }
+
+        /** Maybe {@code null} */
+        public Token getOpeningParenthesis() {
+            return openingParenthesis;
+        }
+
+        /** Maybe {@code null} */
+        public Token getClosingParenthesis() {
+            return closingParenthesis;
+        }
+
+        public List<Identifier> getParameters() {
+            return parameters;
+        }
+
+        public String getCanonicalForm() {
+            if (parameters.size() == 1) {
+                return parameters.get(0).getCanonicalForm();
+            } else {
+                StringBuilder sb = new StringBuilder();
+                sb.append('(');
+                for (int i = 0; i < parameters.size(); i++) {
+                    if (i != 0) {
+                        sb.append(", ");
+                    }
+                    Identifier parameter = parameters.get(i);
+                    sb.append(parameter.getCanonicalForm());
+                }
+                sb.append(')');
+                return sb.toString();
+            }
+        }
+    }
+
+}
diff --git a/src/main/java/freemarker/core/MethodCall.java 
b/src/main/java/freemarker/core/MethodCall.java
index afe3c83..74e20a4 100644
--- a/src/main/java/freemarker/core/MethodCall.java
+++ b/src/main/java/freemarker/core/MethodCall.java
@@ -64,7 +64,7 @@ final class MethodCall extends Expression {
         } else if (targetModel instanceof Macro) {
             return env.invokeFunction(env, (Macro) targetModel, 
arguments.items, this);
         } else {
-            throw new NonMethodException(target, targetModel, true, null, env);
+            throw new NonMethodException(target, targetModel, true, false, 
null, env);
         }
     }
 
diff --git a/src/main/java/freemarker/core/NonMethodException.java 
b/src/main/java/freemarker/core/NonMethodException.java
index a6eff89..4ba3009 100644
--- a/src/main/java/freemarker/core/NonMethodException.java
+++ b/src/main/java/freemarker/core/NonMethodException.java
@@ -59,7 +59,7 @@ public class NonMethodException extends 
UnexpectedTypeException {
 
     NonMethodException(
             Expression blamed, TemplateModel model, String[] tips, Environment 
env) throws InvalidReferenceException {
-        this(blamed, model, false, tips, env);
+        this(blamed, model, false, false, tips, env);
     }
 
     /**
@@ -68,10 +68,11 @@ public class NonMethodException extends 
UnexpectedTypeException {
      * @since 2.3.29
      */
     NonMethodException(
-            Expression blamed, TemplateModel model, boolean allowFTLFunction, 
String[] tips, Environment env)
+            Expression blamed, TemplateModel model, boolean allowFTLFunction, 
boolean allowLambdaExp,
+            String[] tips, Environment env)
             throws InvalidReferenceException {
         super(blamed, model,
-                allowFTLFunction ? "method or function" : "method",
+                "method" + (allowFTLFunction ? " or function" : "") + 
(allowLambdaExp ? " or lambda expression" : ""),
                 allowFTLFunction ? EXPECTED_TYPES_WITH_FUNCTION : 
EXPECTED_TYPES,
                 tips, env);
     }
diff --git a/src/main/java/freemarker/core/SingleIterationCollectionModel.java 
b/src/main/java/freemarker/core/SingleIterationCollectionModel.java
new file mode 100644
index 0000000..292bcd3
--- /dev/null
+++ b/src/main/java/freemarker/core/SingleIterationCollectionModel.java
@@ -0,0 +1,51 @@
+/*
+ * 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;
+import freemarker.template.utility.NullArgumentException;
+
+/**
+ * Used where we really want to return/pass a {@link TemplateModelIterator}, 
but the API requires us to return
+ * a {@link TemplateModel}.
+ *
+ * @since 2.3.29
+ */
+class SingleIterationCollectionModel implements TemplateCollectionModel {
+    private TemplateModelIterator iterator;
+
+    public SingleIterationCollectionModel(TemplateModelIterator iterator) {
+        NullArgumentException.check(iterator);
+        this.iterator = iterator;
+    }
+
+    public TemplateModelIterator iterator() throws TemplateModelException {
+        if (iterator == null) {
+            throw new IllegalStateException(
+                    "Can't return the iterator again, as this 
TemplateCollectionModel can only be iterated once.");
+        }
+        TemplateModelIterator result = iterator;
+        iterator = null;
+        return result;
+    }
+}
diff --git a/src/main/javacc/FTL.jj b/src/main/javacc/FTL.jj
index a2abb4d..a157871 100644
--- a/src/main/javacc/FTL.jj
+++ b/src/main/javacc/FTL.jj
@@ -29,6 +29,7 @@ PARSER_BEGIN(FMParser)
 
 package freemarker.core;
 
+import freemarker.core.LocalLambdaExpression.LambdaParameterList;
 import freemarker.template.*;
 import freemarker.template.utility.*;
 import java.io.*;
@@ -1278,6 +1279,8 @@ TOKEN:
     |
     <ESCAPED_GTE : "gte" | "\\gte" | "&gt;=">
     |
+    <LAMBDA_ARROW : "->" | "-&gt;">
+    |
     <PLUS : "+">
     |
     <MINUS : "-">
@@ -1993,9 +1996,6 @@ Expression RangeExpression() :
     }
 }
 
-
-
-
 Expression AndExpression() :
 {
     Expression lhs, rhs, result;
@@ -2122,8 +2122,10 @@ BuiltinVariable BuiltinVariable() :
             parseTimeValue = new SimpleScalar(outputFormat.getName());
         } else if (nameStr.equals(BuiltinVariable.AUTO_ESC) || 
nameStr.equals(BuiltinVariable.AUTO_ESC_CC)) {
             parseTimeValue = autoEscaping ? TemplateBooleanModel.TRUE : 
TemplateBooleanModel.FALSE;
-        } else {
            parseTimeValue = null;
-        }
        
+        } else {
+            parseTimeValue = null;
+        }
+        
         result = new BuiltinVariable(name, token_source, parseTimeValue);
         
         result.setLocation(template, dot, name);
@@ -2235,13 +2237,31 @@ Expression BuiltIn(Expression lhoExp) :
         }
     }
     [
-        LOOKAHEAD({ result instanceof BuiltInWithParseTimeParameters  })
+        LOOKAHEAD({
+                result instanceof BuiltInWithParseTimeParameters
+                && !((BuiltInWithParseTimeParameters) 
result).isLocalLambdaParameterSupported() })
         openParen = <OPEN_PAREN>
         args = PositionalArgs()
         closeParen = <CLOSE_PAREN> {
             result.setLocation(template, lhoExp, closeParen);
             ((BuiltInWithParseTimeParameters) result).bindToParameters(args, 
openParen, closeParen);
-            
+
+            return result;
+        }
+    ]
+    // In principle we should embed the BuiltInWithParseTimeParameters 
LOOKAHEAD into the
+    // isLocalLambdaParameterSupported LOOKAHEAD, but then the `result` 
variable was out of scope in the code
+    // generated for the nested LOOKAHEAD. So we had to flatten this by 
checking isLocalLambdaParameterSupported twice.
+    [
+        LOOKAHEAD({
+                result instanceof BuiltInWithParseTimeParameters
+                && ((BuiltInWithParseTimeParameters) 
result).isLocalLambdaParameterSupported() })
+        openParen = <OPEN_PAREN>
+        args = PositionalMaybeLambdaArgs()
+        closeParen = <CLOSE_PAREN> {
+            result.setLocation(template, lhoExp, closeParen);
+            ((BuiltInWithParseTimeParameters) result).bindToParameters(args, 
openParen, closeParen);
+
             return result;
         }
     ]
@@ -2251,6 +2271,78 @@ Expression BuiltIn(Expression lhoExp) :
     }
 }
 
+// Only supported as the argument of certain built-ins, so it's not called 
inside Expression.
+Expression LocalLambdaExpression() :
+{
+    LambdaParameterList lhs;
+    Expression rhs, result;
+}
+{
+    (
+        LOOKAHEAD(LambdaExpressionParameterList() <LAMBDA_ARROW>)
+        (
+            lhs = LambdaExpressionParameterList()
+            <LAMBDA_ARROW>
+            rhs = OrExpression()
+            {
+    result = new LocalLambdaExpression(lhs, rhs);
+    if (lhs.getOpeningParenthesis() != null) {
+        // (args) -> exp
+        result.setLocation(template, lhs.getOpeningParenthesis(), rhs);
+    } else {
+        // singleArg -> exp
+        result.setLocation(template, lhs.getParameters().get(0), rhs);
+    }
+}
+        )
+        |
+        result = OrExpression()
+    )
+    {
+        return result;
+    }
+}
+
+LambdaParameterList LambdaExpressionParameterList() :
+{
+    Token openParen = null;
+    Token closeParen = null;
+    List<Identifier> params = null;
+    Identifier param;
+}
+{
+    (
+        (
+            openParen = <OPEN_PAREN>
+            [
+                param = Identifier()
+                {
+                    params = new ArrayList<Identifier>(4);
+                    params.add(param);
+                }
+                (
+                    <COMMA>
+                    param = Identifier()
+                    {
+                        params.add(param);
+                    }
+                )*
+            ]
+            closeParen = <CLOSE_PAREN>
+        )
+        |
+        param = Identifier()
+        {
+            params = Collections.<Identifier>singletonList(param);
+        }
+    )
+    {
+        return new LambdaParameterList(
+                openParen,
+                params != null ? params : Collections.<Identifier>emptyList(),
+                closeParen);
+    }
+}
 
 /**
  * production for when a key is specified by <DOT> + keyname
@@ -3636,6 +3728,27 @@ ArrayList PositionalArgs() :
     }
 }
 
+/**
+ * Like PositionalArgs, but allows lambdas. This is separate as it's slower, 
while lambdas are only allowed on a few
+ * places.
+ */
+ArrayList PositionalMaybeLambdaArgs() :
+{
+    ArrayList result = new ArrayList();
+    Expression arg;
+}
+{
+    [
+        arg = LocalLambdaExpression() { result.add(arg); }
+        (
+            [<COMMA>]
+            arg = LocalLambdaExpression() { result.add(arg); }
+        )*
+    ]
+    {
+        return result;
+    }
+}
 
 Comment Comment() :
 {
diff --git a/src/test/java/freemarker/core/FilterBiTest.java 
b/src/test/java/freemarker/core/FilterBiTest.java
new file mode 100644
index 0000000..14e37ac
--- /dev/null
+++ b/src/test/java/freemarker/core/FilterBiTest.java
@@ -0,0 +1,142 @@
+/*
+ * 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.util.List;
+
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+
+import freemarker.template.TemplateException;
+import freemarker.test.TemplateTest;
+
+public class FilterBiTest extends TemplateTest {
+
+    private static class TestParam {
+        private final List<?> list;
+        private final String result;
+
+        public TestParam(List<?> list, String result) {
+            this.list = list;
+            this.result = result;
+        }
+    }
+
+    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"),
+            new TestParam(ImmutableList.of("aX", "bX", "a", "b", "c", "cX", 
"cX"), "a, b, c"),
+            new TestParam(ImmutableList.of("aX", "bX", "cX"), ""),
+            new TestParam(ImmutableList.of(), "")
+    );
+
+    @Test
+    public void testFilterWithLambda() throws Exception {
+        for (TestParam testParam : TEST_PARAMS) {
+            addToDataModel("xs", testParam.list);
+            assertOutput(
+                    "<#list xs?filter(it -> !it?contains('X')) as 
x>${x}<#sep>, </#list>",
+                    testParam.result);
+            assertOutput(
+                    "<#assign fxs = xs?filter(it -> !it?contains('X'))>" +
+                    "${fxs?join(', ')}",
+                    testParam.result);
+        }
+    }
+
+    @Test
+    public void testFilterWithFunction() throws Exception {
+        for (TestParam testParam : TEST_PARAMS) {
+            addToDataModel("xs", testParam.list);
+            String functionDef = "<#function noX s><#return 
!s?contains('X')></#function>";
+            assertOutput(
+                    functionDef +
+                    "<#list xs?filter(noX) as x>${x}<#sep>, </#list>",
+                    testParam.result);
+            assertOutput(
+                    functionDef +
+                    "<#assign fxs = xs?filter(noX)>" +
+                    "${fxs?join(', ')}",
+                    testParam.result);
+        }
+    }
+
+    @Test
+    public void testFilterWithMethod() throws Exception {
+        for (TestParam testParam : TEST_PARAMS) {
+            addToDataModel("xs", testParam.list);
+            addToDataModel("obj", new FilterObject());
+            assertOutput(
+                    "<#list xs?filter(obj.noX) as x>${x}<#sep>, </#list>",
+                    testParam.result);
+            assertOutput(
+                    "<#assign fxs = xs?filter(obj.noX)>" +
+                    "${fxs?join(', ')}",
+                    testParam.result);
+        }
+    }
+
+    @Test
+    public void testWithNumberElements() throws Exception {
+        addToDataModel("xs", ImmutableList.of(1, 1.5, 2, 2.3, 3));
+        addToDataModel("obj", new FilterObject());
+        assertOutput(
+                "<#list xs?filter(n -> n == n?int) as x>${x}<#sep>, </#list>",
+                "1, 2, 3");
+        assertOutput(
+                "<#function isInteger n><#return n == n?int></#function>" +
+                "<#list xs?filter(isInteger) as x>${x}<#sep>, </#list>",
+                "1, 2, 3");
+        assertOutput(
+                "<#list xs?filter(obj.isInteger) as x>${x}<#sep>, </#list>",
+                "1, 2, 3");
+    }
+
+    @Test
+    public void testErrorMessages() {
+        assertErrorContains("${1?filter(it -> true)}", TemplateException.class,
+                "sequence or collection", "number");
+        assertErrorContains("${[]?filter(1)}", TemplateException.class,
+                "method or function or lambda", "number");
+        assertErrorContains("${['x']?filter(it -> 1)}", 
TemplateException.class,
+                "boolean", "number");
+        assertErrorContains("<#function f></#function>${['x']?filter(f)}", 
TemplateException.class,
+                "Function", "0 parameters", "1");
+        assertErrorContains("<#function f x y 
z></#function>${['x']?filter(f)}", TemplateException.class,
+                "function", "parameter \"y\"");
+        assertErrorContains("<#function f x></#function>${['x']?filter(f)}", 
TemplateException.class,
+                "boolean", "null");
+        assertErrorContains("${[]?filter(() -> true)}", ParseException.class,
+                "lambda", "1 parameter", "declared 0");
+        assertErrorContains("${[]?filter((i, j) -> true)}", 
ParseException.class,
+                "lambda", "1 parameter", "declared 2");
+    }
+
+    public static class FilterObject {
+        public boolean noX(String s) {
+            return !s.contains("X");
+        }
+        public boolean isInteger(double n) {
+            return n == (int) n;
+        }
+    }
+
+}
diff --git a/src/test/java/freemarker/core/MapBiTest.java 
b/src/test/java/freemarker/core/MapBiTest.java
new file mode 100644
index 0000000..56bca58
--- /dev/null
+++ b/src/test/java/freemarker/core/MapBiTest.java
@@ -0,0 +1,258 @@
+/*
+ * 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.math.BigDecimal;
+import java.util.List;
+
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+
+import freemarker.template.Configuration;
+import freemarker.template.TemplateException;
+import freemarker.test.TemplateTest;
+
+public class MapBiTest extends TemplateTest {
+
+    private static class TestParam {
+        private final List<?> list;
+        private final String result;
+
+        public TestParam(List<?> list, String result) {
+            this.list = list;
+            this.result = result;
+        }
+    }
+
+    private static final List<TestParam> TEST_PARAMS = ImmutableList.of(
+            new TestParam(ImmutableList.of("a", "b", "c"), "A, B, C"),
+            new TestParam(ImmutableList.of("a"), "A"),
+            new TestParam(ImmutableList.of(), "")
+    );
+
+    @Override
+    protected Configuration createConfiguration() throws Exception {
+        Configuration cfg = super.createConfiguration();
+        cfg.setNumberFormat("0.####");
+        cfg.setBooleanFormat("c");
+        return cfg;
+    }
+
+    @Test
+    public void testFilterWithLambda() throws Exception {
+        for (TestParam testParam : TEST_PARAMS) {
+            addToDataModel("xs", testParam.list);
+            // Lazy:
+            assertOutput(
+                    "<#list xs?map(it -> it?upperCase) as x>${x}<#sep>, 
</#list>",
+                    testParam.result);
+            // Eager:
+            assertOutput(
+                    "<#assign fxs = xs?map(it -> it?upperCase)>" +
+                    "${fxs?join(', ')}",
+                    testParam.result);
+        }
+    }
+
+    @Test
+    public void testFilterWithFunction() throws Exception {
+        for (TestParam testParam : TEST_PARAMS) {
+            addToDataModel("xs", testParam.list);
+            String functionDef = "<#function toUpper s><#return 
s?upperCase></#function>";
+            // Lazy:
+            assertOutput(
+                    functionDef +
+                    "<#list xs?map(toUpper) as x>${x}<#sep>, </#list>",
+                    testParam.result);
+            // Eager:
+            assertOutput(
+                    functionDef +
+                    "<#assign fxs = xs?map(toUpper)>" +
+                    "${fxs?join(', ')}",
+                    testParam.result);
+        }
+    }
+
+    @Test
+    public void testFilterWithMethod() throws Exception {
+        for (TestParam testParam : TEST_PARAMS) {
+            addToDataModel("xs", testParam.list);
+            addToDataModel("obj", new MapperObject());
+            // Lazy:
+            assertOutput(
+                    "<#list xs?map(obj.toUpper) as x>${x}<#sep>, </#list>",
+                    testParam.result);
+            // Eager:
+            assertOutput(
+                    "<#assign fxs = xs?map(obj.toUpper)>" +
+                    "${fxs?join(', ')}",
+                    testParam.result);
+        }
+    }
+
+    @Test
+    public void testWithNumberElements() throws Exception {
+        addToDataModel("xs", ImmutableList.of(1, 1.55, 3));
+        addToDataModel("obj", new MapperObject());
+        assertOutput(
+                "<#list xs?map(n -> n * 10) as x>${x}<#sep>, </#list>",
+                "10, 15.5, 30");
+        assertOutput(
+                "<#function tenTimes n><#return n * 10></#function>" +
+                "<#list xs?map(tenTimes) as x>${x}<#sep>, </#list>",
+                "10, 15.5, 30");
+        assertOutput(
+                "<#list xs?map(obj.tenTimes) as x>${x}<#sep>, </#list>",
+                "10, 15.5, 30");
+    }
+
+    @Test
+    public void testWithBeanElements() throws Exception {
+        addToDataModel("xs", ImmutableList.of(new User("a"), new User("b"), 
new User("c")));
+        addToDataModel("obj", new MapperObject());
+        assertOutput(
+                "<#list xs?map(user -> user.name) as x>${x}<#sep>, </#list>",
+                "a, b, c");
+        assertOutput(
+                "<#function extractName user><#return user.name></#function>" +
+                        "<#list xs?map(extractName) as x>${x}<#sep>, </#list>",
+                "a, b, c");
+        assertOutput(
+                "<#list xs?map(obj.extractName) as x>${x}<#sep>, </#list>",
+                "a, b, c");
+    }
+
+    @Test
+    public void testBuiltInsThatAllowLazyEval() throws Exception {
+        assertOutput("" +
+                "<#assign s = ''>" +
+                "<#function tenTimes(x)><#assign s += '${x};'><#return x * 
10></#function>" +
+                "${(1..3)?map(tenTimes)?first} ${s}", "10 1;");
+
+        assertOutput("" +
+                "<#assign s = ''>" +
+                "<#function tenTimes(x)><#assign s += '${x};'><#return x * 
10></#function>" +
+                "${(1..3)?map(tenTimes)?seqContains(20)} ${s}", "true 1;2;");
+
+        assertOutput("" +
+                "<#assign s = ''>" +
+                "<#function tenTimes(x)><#assign s += '${x};'><#return x * 
10></#function>" +
+                "${(1..3)?map(tenTimes)?seqIndexOf(20)} ${s}", "1 1;2;");
+
+        // For these this test can't check that there was no sequence built, 
but at least we know they are working:
+        assertOutput("${(1..3)?map(it -> it * 10)?min}", "10");
+        assertOutput("${(1..3)?map(it -> it * 10)?max}", "30");
+        assertOutput("${(1..3)?map(it -> it * 10)?join(', ')}", "10, 20, 30");
+    }
+
+    @Test
+    public void testLaziness() throws Exception {
+        // #list enables lazy evaluation:
+        assertOutput(
+                "" +
+                        "<#assign s = ''>" +
+                        "<#function tenTimes(x)><#assign s += 
'${x}->'><#return x * 10></#function>" +
+                        "<#list (1..3)?map(tenTimes) as x>" +
+                            "<#assign s += x>" +
+                            "<#sep><#assign s += ', '>" +
+                        "</#list>" +
+                        "${s}",
+                "1->10, 2->20, 3->30");
+        // Most other context causes eager behavior:
+        assertOutput(
+                "" +
+                        "<#assign s = ''>" +
+                        "<#function tenTimes(x)><#assign s += 
'${x}->'><#return x * 10></#function>" +
+                        "<#assign xs = (1..3)?map(tenTimes)>" +
+                        "<#list xs as x>" +
+                        "<#assign s += x>" +
+                        "<#sep><#assign s += ', '>" +
+                        "</#list>" +
+                        "${s}",
+                "1->2->3->10, 20, 30");
+
+        // ?map-s can be chained and all is "streaming":
+        assertOutput(
+                "" +
+                        "<#assign s = ''>" +
+                        "<#function tenTimes(x)><#assign s += 
'${x}->'><#return x * 10></#function>" +
+                        "<#list 
(1..3)?map(tenTimes)?map(tenTimes)?map(tenTimes) as x>" +
+                            "<#assign s += x>" +
+                            "<#sep><#assign s += ', '>" +
+                        "</#list>" +
+                "${s}",
+                "1->10->100->1000, 2->20->200->2000, 3->30->300->3000");
+
+        // Rest of the elements not consumed after #break:
+        assertOutput(
+                "" +
+                        "<#assign s = ''>" +
+                        "<#function tenTimes(x)><#assign s += 
'${x}->'><#return x * 10></#function>" +
+                        "<#list (1..3)?map(tenTimes) as x>" +
+                            "<#assign s += x>" +
+                            "<#sep><#assign s += ', '>" +
+                            "<#if x == 20><#break></#if>" +
+                        "</#list>" +
+                        "${s}",
+                "1->10, 2->20, ");
+    }
+
+    @Test
+    public void testErrorMessages() {
+        assertErrorContains("${1?map(it -> it)}", TemplateException.class,
+                "sequence or collection", "number");
+        assertErrorContains("${[]?map(1)}", TemplateException.class,
+                "method or function or lambda", "number");
+        assertErrorContains("<#function f></#function>${['x']?map(f)}", 
TemplateException.class,
+                "Function", "0 parameters", "1");
+        assertErrorContains("<#function f x y z></#function>${['x']?map(f)}", 
TemplateException.class,
+                "function", "parameter \"y\"");
+        assertErrorContains("<#function f x></#function>${['x']?map(f)}", 
TemplateException.class,
+                "null");
+        assertErrorContains("${[]?map(() -> 1)}", ParseException.class,
+                "lambda", "1 parameter", "declared 0");
+        assertErrorContains("${[]?map((i, j) -> 1)}", ParseException.class,
+                "lambda", "1 parameter", "declared 2");
+    }
+
+    public static class MapperObject {
+        public String toUpper(String s) {
+            return s.toUpperCase();
+        }
+        public BigDecimal tenTimes(BigDecimal n) {
+            return n.movePointRight(1);
+        }
+        public String extractName(User user) { return user.getName(); }
+    }
+
+    public static class User {
+        private final String name;
+
+        public User(String name) {
+            this.name = name;
+        }
+
+        public String getName() {
+            return name;
+        }
+    }
+
+}

Reply via email to