You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2019/02/23 16:31:20 UTC
[freemarker] 02/03: 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 a fe [...]
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 <dd...@apache.org>
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" | ">=">
|
+ <LAMBDA_ARROW : "->" | "->">
+ |
<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;
+ }
+ }
+
+}