This is an automated email from the ASF dual-hosted git repository.
ddekany pushed a commit to branch 2.3-gae
in repository https://gitbox.apache.org/repos/asf/freemarker.git
The following commit(s) were added to refs/heads/2.3-gae by this push:
new 54905b6 Added ?spread_args, though it's likely not the final version
54905b6 is described below
commit 54905b601b467692fd8f9c25618fd7e524d00acb
Author: ddekany <[email protected]>
AuthorDate: Sun Sep 22 23:49:34 2019 +0200
Added ?spread_args, though it's likely not the final version
---
src/main/java/freemarker/core/BuiltIn.java | 6 +-
.../java/freemarker/core/BuiltInsForCallables.java | 226 +++++++++++
src/main/java/freemarker/core/Environment.java | 190 +++++++--
src/main/java/freemarker/core/EvalUtil.java | 6 +-
src/main/java/freemarker/core/Macro.java | 96 ++++-
src/main/java/freemarker/core/TemplateElement.java | 23 +-
src/main/java/freemarker/core/TemplateObject.java | 16 +-
src/main/java/freemarker/core/UnifiedCall.java | 13 +-
src/main/java/freemarker/core/_MessageUtil.java | 7 +-
.../freemarker/template/SimpleObjectWrapper.java | 6 +-
.../template/TemplateDirectiveModel.java | 2 +-
src/manual/en_US/book.xml | 81 +++-
.../freemarker/core/SpreadArgsBuiltInTest.java | 442 +++++++++++++++++++++
13 files changed, 1041 insertions(+), 73 deletions(-)
diff --git a/src/main/java/freemarker/core/BuiltIn.java
b/src/main/java/freemarker/core/BuiltIn.java
index 30c9c9d..f1be894 100644
--- a/src/main/java/freemarker/core/BuiltIn.java
+++ b/src/main/java/freemarker/core/BuiltIn.java
@@ -84,9 +84,12 @@ 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 = 285;
+ static final int NUMBER_OF_BIS = 287;
static final HashMap<String, BuiltIn> BUILT_INS_BY_NAME = new
HashMap(NUMBER_OF_BIS * 3 / 2 + 1, 1f);
+ static final String BI_NAME_SNAKE_CASE_SPREAD_ARGS = "spread_args";
+ static final String BI_NAME_CAMEL_CASE_SPREAD_ARGS = "spreadArgs";
+
static {
// Note that you must update NUMBER_OF_BIS if you add new items here!
@@ -275,6 +278,7 @@ abstract class BuiltIn extends Expression implements
Cloneable {
putBI("sort_by", "sortBy", new sort_byBI());
putBI("sort", new sortBI());
putBI("split", new BuiltInsForStringsBasic.split_BI());
+ putBI(BI_NAME_SNAKE_CASE_SPREAD_ARGS, BI_NAME_CAMEL_CASE_SPREAD_ARGS,
new BuiltInsForCallables.spread_argsBI());
putBI("switch", new BuiltInsWithLazyConditionals.switch_BI());
putBI("starts_with", "startsWith", new
BuiltInsForStringsBasic.starts_withBI());
putBI("string", new BuiltInsForMultipleTypes.stringBI());
diff --git a/src/main/java/freemarker/core/BuiltInsForCallables.java
b/src/main/java/freemarker/core/BuiltInsForCallables.java
new file mode 100644
index 0000000..477f103
--- /dev/null
+++ b/src/main/java/freemarker/core/BuiltInsForCallables.java
@@ -0,0 +1,226 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.core;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import freemarker.template.TemplateDirectiveBody;
+import freemarker.template.TemplateDirectiveModel;
+import freemarker.template.TemplateException;
+import freemarker.template.TemplateHashModelEx;
+import freemarker.template.TemplateHashModelEx2;
+import freemarker.template.TemplateMethodModel;
+import freemarker.template.TemplateMethodModelEx;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateScalarModel;
+import freemarker.template.TemplateSequenceModel;
+import freemarker.template.utility.TemplateModelUtils;
+
+class BuiltInsForCallables {
+
+ static class spread_argsBI extends BuiltIn {
+
+ TemplateModel _eval(Environment env) throws TemplateException {
+ TemplateModel model = target.eval(env);
+ if (model instanceof Macro) {
+ return new BIMethodForMacroAndFunction((Macro) model);
+ } else if (model instanceof TemplateDirectiveModel) {
+ return new BIMethodForDirective((TemplateDirectiveModel)
model);
+ } else if (model instanceof TemplateMethodModel) {
+ return new BIMethodForMethod((TemplateMethodModel) model);
+ } else {
+ throw new UnexpectedTypeException(
+ target, model,
+ "macro, function, directive, or method", new Class[] {
Macro.class,
+ TemplateDirectiveModel.class,
TemplateMethodModel.class },
+ env);
+ }
+ }
+
+ private class BIMethodForMacroAndFunction implements
TemplateMethodModelEx {
+
+ private final Macro macroOrFunction;
+
+ private BIMethodForMacroAndFunction(Macro macroOrFunction) {
+ this.macroOrFunction = macroOrFunction;
+ }
+
+ public Object exec(List args) throws TemplateModelException {
+ checkMethodArgCount(args.size(), 1);
+ TemplateModel argTM = (TemplateModel) args.get(0);
+
+ Macro.SpreadArgs spreadArgs;
+ if (argTM instanceof TemplateSequenceModel) {
+ spreadArgs = new Macro.SpreadArgs((TemplateSequenceModel)
argTM);
+ } else if (argTM instanceof TemplateHashModelEx) {
+ if (macroOrFunction.isFunction()) {
+ throw new _TemplateModelException("When applied on a
function, ?", key,
+ " can't have a hash argument. Use a sequence
argument.");
+ }
+ spreadArgs = new Macro.SpreadArgs((TemplateHashModelEx)
argTM);
+ } else {
+ throw
_MessageUtil.newMethodArgMustBeExtendedHashOrSequnceException("?" + key, 0,
argTM);
+ }
+
+ return new Macro(macroOrFunction, spreadArgs);
+ }
+
+ }
+
+ private class BIMethodForMethod implements TemplateMethodModelEx {
+
+ private final TemplateMethodModel method;
+
+ public BIMethodForMethod(TemplateMethodModel method) {
+ this.method = method;
+ }
+
+ public Object exec(List args) throws TemplateModelException {
+ checkMethodArgCount(args.size(), 1);
+ TemplateModel argTM = (TemplateModel) args.get(0);
+
+ if (argTM instanceof TemplateSequenceModel) {
+ final TemplateSequenceModel spreadArgs =
(TemplateSequenceModel) argTM;
+ if (method instanceof TemplateMethodModelEx) {
+ return new TemplateMethodModelEx() {
+ public Object exec(List origArgs) throws
TemplateModelException {
+ int spreadArgsSize = spreadArgs.size();
+ List<TemplateModel> newArgs = new
ArrayList<TemplateModel>(
+ spreadArgsSize + origArgs.size());
+
+ for (int i = 0; i < spreadArgsSize; i++) {
+ newArgs.add(spreadArgs.get(i));
+ }
+
+ newArgs.addAll(origArgs);
+
+ return method.exec(newArgs);
+ }
+ };
+ } else {
+ return new TemplateMethodModel() {
+ public Object exec(List origArgs) throws
TemplateModelException {
+ int spreadArgsSize = spreadArgs.size();
+ List<String> newArgs = new ArrayList<String>(
+ spreadArgsSize + origArgs.size());
+
+ for (int i = 0; i < spreadArgsSize; i++) {
+ TemplateModel argVal = spreadArgs.get(i);
+ newArgs.add(argValueToString(argVal));
+ }
+
+ newArgs.addAll(origArgs);
+
+ return method.exec(newArgs);
+ }
+
+ /**
+ * Mimics the behavior of method call expression
when it calls legacy method model.
+ */
+ private String argValueToString(TemplateModel
argVal) throws TemplateModelException {
+ String argValStr;
+ if (argVal instanceof TemplateScalarModel) {
+ argValStr = ((TemplateScalarModel)
argVal).getAsString();
+ } else if (argVal == null) {
+ argValStr = null;
+ } else {
+ try {
+ argValStr =
EvalUtil.coerceModelToPlainText(argVal, null, null,
+
Environment.getCurrentEnvironment());
+ } catch (TemplateException e) {
+ throw new _TemplateModelException(e,
+ "Failed to convert method
argument to string. Argument type was: ",
+ new
_DelayedFTLTypeDescription(argVal));
+ }
+ }
+ return argValStr;
+ }
+ };
+ }
+ } else if (argTM instanceof TemplateHashModelEx) {
+ throw new _TemplateModelException("When applied on a
method, ?", key,
+ " can't have a hash argument. Use a sequence
argument.");
+ } else {
+ throw
_MessageUtil.newMethodArgMustBeExtendedHashOrSequnceException("?" + key, 0,
argTM);
+ }
+ }
+
+ }
+
+ private class BIMethodForDirective implements TemplateMethodModelEx {
+
+ private final TemplateDirectiveModel directive;
+
+ public BIMethodForDirective(TemplateDirectiveModel directive) {
+ this.directive = directive;
+ }
+
+ public Object exec(List args) throws TemplateModelException {
+ checkMethodArgCount(args.size(), 1);
+ TemplateModel argTM = (TemplateModel) args.get(0);
+
+ if (argTM instanceof TemplateHashModelEx) {
+ final TemplateHashModelEx spreadArgs =
(TemplateHashModelEx) argTM;
+ return new TemplateDirectiveModel() {
+ public void execute(Environment env, Map origArgs,
TemplateModel[] loopVars,
+ TemplateDirectiveBody body) throws
TemplateException, IOException {
+ int spreadArgsSize = spreadArgs.size();
+ Map<String, TemplateModel> newArgs = new
LinkedHashMap<String, TemplateModel>(
+ (spreadArgsSize + origArgs.size()) * 4 /
3, 1f);
+
+ TemplateHashModelEx2.KeyValuePairIterator
spreadArgsIter =
+
TemplateModelUtils.getKeyValuePairIterator(spreadArgs);
+ while (spreadArgsIter.hasNext()) {
+ TemplateHashModelEx2.KeyValuePair spreadArgKVP
= spreadArgsIter.next();
+
+ TemplateModel argNameTM =
spreadArgKVP.getKey();
+ if (!(argNameTM instanceof
TemplateScalarModel)) {
+ throw new _TemplateModelException(
+ "Expected string keys in the
spread args hash, but one of the keys was ",
+ new _DelayedAOrAn(new
_DelayedFTLTypeDescription(argNameTM)), ".");
+ }
+ String argName =
EvalUtil.modelToString((TemplateScalarModel) argNameTM, null, null);
+
+ newArgs.put(argName, spreadArgKVP.getValue());
+ }
+
+ newArgs.putAll(origArgs); // TODO Should null
replace non-null?
+
+ directive.execute(env, newArgs, loopVars, body);
+ }
+ };
+ } else if (argTM instanceof TemplateSequenceModel) {
+ throw new _TemplateModelException("When applied on a
directive, ?", key,
+ " can't have a sequence argument. Use a hash
argument.");
+ } else {
+ throw
_MessageUtil.newMethodArgMustBeExtendedHashOrSequnceException("?" + key, 0,
argTM);
+ }
+ }
+
+ }
+
+ }
+
+}
diff --git a/src/main/java/freemarker/core/Environment.java
b/src/main/java/freemarker/core/Environment.java
index c7bfc1a..a048681 100644
--- a/src/main/java/freemarker/core/Environment.java
+++ b/src/main/java/freemarker/core/Environment.java
@@ -61,6 +61,7 @@ import freemarker.template.TemplateException;
import freemarker.template.TemplateExceptionHandler;
import freemarker.template.TemplateHashModel;
import freemarker.template.TemplateHashModelEx;
+import freemarker.template.TemplateHashModelEx2;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateModelIterator;
@@ -75,6 +76,7 @@ import freemarker.template.utility.DateUtil;
import freemarker.template.utility.DateUtil.DateToISO8601CalendarFactory;
import freemarker.template.utility.NullWriter;
import freemarker.template.utility.StringUtil;
+import freemarker.template.utility.TemplateModelUtils;
import freemarker.template.utility.UndeclaredThrowableException;
/**
@@ -171,7 +173,7 @@ public final class Environment extends Configurable {
private Throwable lastThrowable;
private TemplateModel lastReturnValue;
- private HashMap macroToNamespaceLookup = new HashMap();
+ private Map<Object, Namespace> macroToNamespaceLookup = new
IdentityHashMap<Object, Namespace>();
private TemplateNodeModel currentVisitorNode;
private TemplateSequenceModel nodeNamespaces;
@@ -804,8 +806,8 @@ public final class Environment extends Configurable {
* Calls a macro with the given arguments and nested block.
*/
void invokeMacro(Macro macro,
- Map namedArgs, List<? extends Expression> positionalArgs,
- List bodyParameterNames, TemplateObject callPlace) throws
TemplateException, IOException {
+ Map<String, ? extends Expression> namedArgs, List<? extends
Expression> positionalArgs,
+ List<String> bodyParameterNames, TemplateObject callPlace) throws
TemplateException, IOException {
invokeMacroOrFunctionCommonPart(macro, namedArgs, positionalArgs,
bodyParameterNames, callPlace);
}
@@ -833,8 +835,9 @@ public final class Environment extends Configurable {
}
private void invokeMacroOrFunctionCommonPart(Macro macroOrFunction,
- Map namedArgs, List<? extends Expression> positionalArgs,
- List<Expression> bodyParameterNames, TemplateObject callPlace)
throws TemplateException, IOException {
+ Map<String, ? extends Expression> namedArgs, List<? extends
Expression> positionalArgs,
+ List<String> bodyParameterNames, TemplateObject callPlace) throws
TemplateException,
+ IOException {
if (macroOrFunction == Macro.DO_NOTHING_MACRO) {
return;
}
@@ -865,7 +868,7 @@ public final class Environment extends Configurable {
localContextStack = null;
final Namespace prevNamespace = currentNamespace;
- currentNamespace = (Namespace)
macroToNamespaceLookup.get(macroOrFunction);
+ currentNamespace = getMacroNamespace(macroOrFunction);
try {
macroCtx.checkParamsSetAndApplyDefaults(this);
@@ -892,62 +895,134 @@ public final class Environment extends Configurable {
private void setMacroContextLocalsFromArguments(
final Macro.Context macroCtx,
final Macro macro,
- final Map namedArgs, final List<? extends Expression>
positionalArgs) throws TemplateException,
- _MiscTemplateException {
+ final Map<String, ? extends Expression> namedArgs, final List<?
extends Expression> positionalArgs)
+ throws TemplateException {
String catchAllParamName = macro.getCatchAll();
+ SimpleHash namedCatchAllParamValue = null;
+ SimpleSequence positionalCatchAllParamValue = null;
+ int nextPositionalArgToAssignIdx = 0;
+
+ // Used for ?spread_args(...):
+ Macro.SpreadArgs spreadArgs = macro.getSpreadArgs();
+ if (spreadArgs != null) {
+ TemplateHashModelEx byNameSpreadArgs = spreadArgs.getByName();
+ TemplateSequenceModel byPositionSpreadArgs =
spreadArgs.getByPosition();
+
+ if (byNameSpreadArgs != null) {
+ new HashMap<String, TemplateModel>(byNameSpreadArgs.size() * 4
/ 3, 1f);
+ TemplateHashModelEx2.KeyValuePairIterator
namedParamValueOverridesIter =
+
TemplateModelUtils.getKeyValuePairIterator(byNameSpreadArgs);
+ while (namedParamValueOverridesIter.hasNext()) {
+ TemplateHashModelEx2.KeyValuePair defaultArgHashKVP =
namedParamValueOverridesIter.next();
+
+ String argName;
+ {
+ TemplateModel argNameTM = defaultArgHashKVP.getKey();
+ if (!(argNameTM instanceof TemplateScalarModel)) {
+ throw new _TemplateModelException(
+ "Expected string keys in the spread args
hash, but one of the keys was ",
+ new _DelayedAOrAn(new
_DelayedFTLTypeDescription(argNameTM)), ".");
+ }
+ argName = EvalUtil.modelToString((TemplateScalarModel)
argNameTM, null, null);
+ }
+
+ TemplateModel argValue = defaultArgHashKVP.getValue();
+ // What if argValue is null? It still has to occur in the
named catch-all parameter, to be similar
+ // to <@macroWithCatchAll a=null b=null />, that will also
add the keys to the catch-all hash.
+ // Similarly, we also still fail if the name is not
declared.
+ final boolean isArgNameDeclared =
macro.hasArgNamed(argName);
+ if (isArgNameDeclared) {
+ macroCtx.setLocalVar(argName, argValue);
+ } else if (catchAllParamName != null) {
+ if (namedCatchAllParamValue == null) {
+ namedCatchAllParamValue =
initNamedCatchAllParameter(macroCtx, catchAllParamName);
+ }
+ namedCatchAllParamValue.put(argName, argValue);
+ } else {
+ throw newUndeclaredParamNameException(macro, argName);
+ }
+ }
+ } else if (byPositionSpreadArgs != null) {
+ String[] argNames = macro.getArgumentNamesInternal();
+ final int argsCnt = byPositionSpreadArgs.size();
+ if (argNames.length < argsCnt && catchAllParamName == null) {
+ throw newTooManyArgumentsException(macro, argNames,
argsCnt);
+ }
+ for (int i = 0; i < argsCnt; i++) {
+ TemplateModel argValue = byPositionSpreadArgs.get(i);
+ try {
+ if (nextPositionalArgToAssignIdx < argNames.length) {
+ String argName =
argNames[nextPositionalArgToAssignIdx++];
+ macroCtx.setLocalVar(argName, argValue);
+ } else {
+ if (positionalCatchAllParamValue == null) {
+ positionalCatchAllParamValue =
initPositionalCatchAllParameter(macroCtx, catchAllParamName);
+ }
+ positionalCatchAllParamValue.add(argValue);
+ }
+ } catch (RuntimeException re) {
+ throw new _MiscTemplateException(re, this);
+ }
+ }
+ }
+ }
+
if (namedArgs != null) {
- final SimpleHash catchAllParamValue;
- if (catchAllParamName != null) {
- catchAllParamValue = new SimpleHash((ObjectWrapper) null);
- macroCtx.setLocalVar(catchAllParamName, catchAllParamValue);
- } else {
- catchAllParamValue = null;
+ if (catchAllParamName != null && namedCatchAllParamValue == null
&& positionalCatchAllParamValue == null) {
+ if (namedArgs.isEmpty() && spreadArgs != null &&
spreadArgs.getByPosition() != null) {
+ positionalCatchAllParamValue =
initPositionalCatchAllParameter(macroCtx, catchAllParamName);
+ } else {
+ namedCatchAllParamValue =
initNamedCatchAllParameter(macroCtx, catchAllParamName);
+ }
}
- for (Iterator it = namedArgs.entrySet().iterator(); it.hasNext();)
{
- final Map.Entry argNameAndValExp = (Map.Entry) it.next();
- final String argName = (String) argNameAndValExp.getKey();
+ for (Map.Entry<String, ? extends Expression> argNameAndValExp :
namedArgs.entrySet()) {
+ final String argName = argNameAndValExp.getKey();
final boolean isArgNameDeclared = macro.hasArgNamed(argName);
- if (isArgNameDeclared || catchAllParamName != null) {
- Expression argValueExp = (Expression)
argNameAndValExp.getValue();
+ if (isArgNameDeclared || namedCatchAllParamValue != null) {
+ final Expression argValueExp = argNameAndValExp.getValue();
TemplateModel argValue = argValueExp.eval(this);
if (isArgNameDeclared) {
macroCtx.setLocalVar(argName, argValue);
} else {
- catchAllParamValue.put(argName, argValue);
+ namedCatchAllParamValue.put(argName, argValue);
}
} else {
- throw new _MiscTemplateException(this,
- (macro.isFunction() ? "Function " : "Macro "), new
_DelayedJQuote(macro.getName()),
- " has no parameter with name ", new
_DelayedJQuote(argName), ".");
+ if (positionalCatchAllParamValue != null) {
+ throw
newBothNamedAndPositionalCatchAllParamsException(macro);
+ } else {
+ throw newUndeclaredParamNameException(macro, argName);
+ }
}
}
} else if (positionalArgs != null) {
- final SimpleSequence catchAllParamValue;
- if (catchAllParamName != null) {
- catchAllParamValue = new SimpleSequence((ObjectWrapper) null);
- macroCtx.setLocalVar(catchAllParamName, catchAllParamValue);
- } else {
- catchAllParamValue = null;
+ if (catchAllParamName != null && positionalCatchAllParamValue ==
null && namedCatchAllParamValue == null) {
+ if (positionalArgs.isEmpty() && spreadArgs != null &&
spreadArgs.getByName() != null) {
+ namedCatchAllParamValue =
initNamedCatchAllParameter(macroCtx, catchAllParamName);
+ } else {
+ positionalCatchAllParamValue =
initPositionalCatchAllParameter(macroCtx, catchAllParamName);
+ }
}
String[] argNames = macro.getArgumentNamesInternal();
final int argsCnt = positionalArgs.size();
- if (argNames.length < argsCnt && catchAllParamName == null) {
- throw new _MiscTemplateException(this,
- (macro.isFunction() ? "Function " : "Macro "), new
_DelayedJQuote(macro.getName()),
- " only accepts ", new
_DelayedToString(argNames.length), " parameters, but got ",
- new _DelayedToString(argsCnt), ".");
+ final int argsWithSpreadArgsCnt = argsCnt +
nextPositionalArgToAssignIdx;
+ if (argNames.length < argsWithSpreadArgsCnt &&
positionalCatchAllParamValue == null) {
+ if (namedCatchAllParamValue != null) {
+ throw
newBothNamedAndPositionalCatchAllParamsException(macro);
+ } else {
+ throw newTooManyArgumentsException(macro, argNames,
argsWithSpreadArgsCnt);
+ }
}
- for (int i = 0; i < argsCnt; i++) {
- Expression argValueExp = positionalArgs.get(i);
+ for (int srcPosArgIdx = 0; srcPosArgIdx < argsCnt; srcPosArgIdx++)
{
+ Expression argValueExp = positionalArgs.get(srcPosArgIdx);
TemplateModel argValue = argValueExp.eval(this);
try {
- if (i < argNames.length) {
- String argName = argNames[i];
+ if (nextPositionalArgToAssignIdx < argNames.length) {
+ String argName =
argNames[nextPositionalArgToAssignIdx++];
macroCtx.setLocalVar(argName, argValue);
} else {
- catchAllParamValue.add(argValue);
+ positionalCatchAllParamValue.add(argValue);
}
} catch (RuntimeException re) {
throw new _MiscTemplateException(re, this);
@@ -956,16 +1031,49 @@ public final class Environment extends Configurable {
}
}
+ private _MiscTemplateException newTooManyArgumentsException(Macro macro,
String[] argNames, int argsCnt) {
+ return new _MiscTemplateException(this,
+ (macro.isFunction() ? "Function " : "Macro "), new
_DelayedJQuote(macro.getName()),
+ " only accepts ", new _DelayedToString(argNames.length), "
parameters, but got ",
+ new _DelayedToString(argsCnt), ".");
+ }
+
+ private static SimpleSequence
initPositionalCatchAllParameter(Macro.Context macroCtx, String
catchAllParamName) {
+ SimpleSequence positionalCatchAllParamValue;
+ positionalCatchAllParamValue = new SimpleSequence((ObjectWrapper)
null);
+ macroCtx.setLocalVar(catchAllParamName, positionalCatchAllParamValue);
+ return positionalCatchAllParamValue;
+ }
+
+ private static SimpleHash initNamedCatchAllParameter(Macro.Context
macroCtx, String catchAllParamName) {
+ SimpleHash namedCatchAllParamValue;
+ namedCatchAllParamValue = new SimpleHash((ObjectWrapper) null);
+ macroCtx.setLocalVar(catchAllParamName, namedCatchAllParamValue);
+ return namedCatchAllParamValue;
+ }
+
+ private _MiscTemplateException newUndeclaredParamNameException(Macro
macro, String argName) {
+ return new _MiscTemplateException(this,
+ (macro.isFunction() ? "Function " : "Macro "), new
_DelayedJQuote(macro.getName()),
+ " has no parameter with name ", new _DelayedJQuote(argName),
".");
+ }
+
+ private _MiscTemplateException
newBothNamedAndPositionalCatchAllParamsException(Macro macro) {
+ return new _MiscTemplateException(this,
+ (macro.isFunction() ? "Function " : "Macro "), new
_DelayedJQuote(macro.getName()),
+ " call can't have both named and positional arguments that has
to go into catch-all parameter.");
+ }
+
/**
* Defines the given macro in the current namespace (doesn't call it).
*/
void visitMacroDef(Macro macro) {
- macroToNamespaceLookup.put(macro, currentNamespace);
+ macroToNamespaceLookup.put(macro.getNamespaceLookupKey(),
currentNamespace);
currentNamespace.put(macro.getName(), macro);
}
Namespace getMacroNamespace(Macro macro) {
- return (Namespace) macroToNamespaceLookup.get(macro);
+ return macroToNamespaceLookup.get(macro.getNamespaceLookupKey());
}
void recurse(TemplateNodeModel node, TemplateSequenceModel namespaces)
diff --git a/src/main/java/freemarker/core/EvalUtil.java
b/src/main/java/freemarker/core/EvalUtil.java
index 0fa7a48..9af8d83 100644
--- a/src/main/java/freemarker/core/EvalUtil.java
+++ b/src/main/java/freemarker/core/EvalUtil.java
@@ -441,7 +441,8 @@ class EvalUtil {
*
* @param seqTip
* Tip to display if the value type is not coercable, but it's
sequence or collection.
- *
+ * @param exp {@code null} is allowed, but may results in less helpful
error messages
+ *
* @return Never {@code null}
*/
static String coerceModelToPlainText(TemplateModel tm, Expression exp,
String seqTip,
@@ -461,7 +462,8 @@ class EvalUtil {
*
* @param supportsTOM
* Whether the caller {@code coerceModelTo...} method could
handle a {@link TemplateMarkupOutputModel}.
- *
+ * @param exp {@code null} is allowed, but may results in less helpful
error messages
+ *
* @return Never {@code null}
*/
private static String coerceModelToTextualCommon(
diff --git a/src/main/java/freemarker/core/Macro.java
b/src/main/java/freemarker/core/Macro.java
index c76092b..6527e80 100644
--- a/src/main/java/freemarker/core/Macro.java
+++ b/src/main/java/freemarker/core/Macro.java
@@ -26,11 +26,14 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import freemarker.template.Configuration;
import freemarker.template.TemplateException;
+import freemarker.template.TemplateHashModelEx;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateModelIterator;
import freemarker.template.TemplateScalarModel;
+import freemarker.template.TemplateSequenceModel;
/**
* An element representing a macro or function declaration.
@@ -50,26 +53,52 @@ public final class Macro extends TemplateElement implements
TemplateModel {
private final String name;
private final String[] paramNames;
- private final Map paramNamesWithDefault;
+ private final Map<String, Expression> paramNamesWithDefault;
+ private final SpreadArgs spreadArgs;
private final String catchAllParamName;
private final boolean function;
+ private final Object namespaceLookupKey;
/**
* @param paramNamesWithDefault Maps the parameter names to its default
value expression, or to {@code null} if
* there's no default value. As parameter order is significant; use
{@link LinkedHashMap} or similar.
* This doesn't include the catch-all parameter (as that can be
specified by name on the caller side).
*/
- Macro(String name, Map<String, Expression> paramNamesWithDefault,
+ Macro(String name,
+ Map<String, Expression> paramNamesWithDefault,
String catchAllParamName, boolean function,
TemplateElements children) {
+ // Attention! Keep this constructor in sync with the other constructor!
this.name = name;
this.paramNamesWithDefault = paramNamesWithDefault;
this.paramNames = paramNamesWithDefault.keySet().toArray(new
String[0]);
this.catchAllParamName = catchAllParamName;
-
+ this.spreadArgs = null;
this.function = function;
-
this.setChildren(children);
+ this.namespaceLookupKey = this;
+ // Attention! Keep this constructor in sync with the other constructor!
+ }
+
+ /**
+ * Copy-constructor with replacing {@link #spreadArgs} (with the quirk
that the parent of the
+ * child elements will stay the copied macro).
+ *
+ * @param spreadArgs Usually {@code null}; used by {@link
BuiltInsForCallables.spread_argsBI} to
+ * set arbitrary default value to parameters. Note that the defaults
aren't
+ * {@link Expression}-s, but {@link TemplateModel}-s.
+ */
+ Macro(Macro that, SpreadArgs spreadArgs) {
+ // Attention! Keep this constructor in sync with the other constructor!
+ this.name = that.name;
+ this.paramNamesWithDefault = that.paramNamesWithDefault;
+ this.paramNames = that.paramNames;
+ this.catchAllParamName = that.catchAllParamName;
+ this.spreadArgs = spreadArgs; // Using the argument value here
+ this.function = that.function;
+ this.namespaceLookupKey = that.namespaceLookupKey;
+ super.copyFieldsFrom(that);
+ // Attention! Keep this constructor in sync with the other constructor!
}
public String getCatchAll() {
@@ -92,6 +121,14 @@ public final class Macro extends TemplateElement implements
TemplateModel {
return name;
}
+ public SpreadArgs getSpreadArgs() {
+ return spreadArgs;
+ }
+
+ public Object getNamespaceLookupKey() {
+ return namespaceLookupKey;
+ }
+
@Override
TemplateElement[] accept(Environment env) {
env.visitMacroDef(this);
@@ -103,6 +140,14 @@ public final class Macro extends TemplateElement
implements TemplateModel {
StringBuilder sb = new StringBuilder();
if (canonical) sb.append('<');
sb.append(getNodeTypeSymbol());
+ if (spreadArgs != null) {
+ // As such a node won't be part of a template, this is probably
never needed.
+ sb.append('?')
+ .append(getTemplate().getActualNamingConvention() ==
Configuration.CAMEL_CASE_NAMING_CONVENTION
+ ? BuiltIn.BI_NAME_CAMEL_CASE_SPREAD_ARGS
+ : BuiltIn.BI_NAME_SNAKE_CASE_SPREAD_ARGS)
+ .append("(...)");
+ }
sb.append(' ');
sb.append(_CoreStringUtils.toFTLTopLevelTragetIdentifier(name));
if (function) sb.append('(');
@@ -115,15 +160,17 @@ public final class Macro extends TemplateElement
implements TemplateModel {
} else {
sb.append(' ');
}
- String argName = paramNames[i];
-
sb.append(_CoreStringUtils.toFTLTopLevelIdentifierReference(argName));
- if (paramNamesWithDefault != null &&
paramNamesWithDefault.get(argName) != null) {
+
+ String paramName = paramNames[i];
+
sb.append(_CoreStringUtils.toFTLTopLevelIdentifierReference(paramName));
+
+ Expression paramDefaultExp = (Expression)
paramNamesWithDefault.get(paramName);
+ if (paramDefaultExp != null) {
sb.append('=');
- Expression defaultExpr = (Expression)
paramNamesWithDefault.get(argName);
if (function) {
- sb.append(defaultExpr.getCanonicalForm());
+ sb.append(paramDefaultExp.getCanonicalForm());
} else {
- _MessageUtil.appendExpressionAsUntearable(sb, defaultExpr);
+ _MessageUtil.appendExpressionAsUntearable(sb,
paramDefaultExp);
}
}
}
@@ -160,13 +207,13 @@ public final class Macro extends TemplateElement
implements TemplateModel {
final Environment.Namespace localVars;
final TemplateObject callPlace;
final Environment.Namespace nestedContentNamespace;
- final List nestedContentParameterNames;
+ final List<String> nestedContentParameterNames;
final LocalContextStack prevLocalContextStack;
final Context prevMacroContext;
Context(Environment env,
TemplateObject callPlace,
- List nestedContentParameterNames) {
+ List<String> nestedContentParameterNames) {
this.localVars = env.new Namespace();
this.callPlace = callPlace;
this.nestedContentNamespace = env.getCurrentNamespace();
@@ -192,7 +239,7 @@ public final class Macro extends TemplateElement implements
TemplateModel {
for (int i = 0; i < paramNames.length; ++i) {
String argName = paramNames[i];
if (localVars.get(argName) == null) {
- Expression defaultValueExp = (Expression)
paramNamesWithDefault.get(argName);
+ Expression defaultValueExp =
paramNamesWithDefault.get(argName);
if (defaultValueExp != null) {
try {
TemplateModel tm = defaultValueExp.eval(env);
@@ -325,5 +372,28 @@ public final class Macro extends TemplateElement
implements TemplateModel {
// Because of recursive calls
return true;
}
+
+ static final class SpreadArgs {
+ private final TemplateHashModelEx byName;
+ private final TemplateSequenceModel byPosition;
+
+ SpreadArgs(TemplateHashModelEx byName) {
+ this.byName = byName;
+ this.byPosition = null;
+ }
+
+ SpreadArgs(TemplateSequenceModel byPosition) {
+ this.byName = null;
+ this.byPosition = byPosition;
+ }
+
+ public TemplateHashModelEx getByName() {
+ return byName;
+ }
+
+ public TemplateSequenceModel getByPosition() {
+ return byPosition;
+ }
+ }
}
diff --git a/src/main/java/freemarker/core/TemplateElement.java
b/src/main/java/freemarker/core/TemplateElement.java
index a8dedea..5f90b60 100644
--- a/src/main/java/freemarker/core/TemplateElement.java
+++ b/src/main/java/freemarker/core/TemplateElement.java
@@ -22,6 +22,7 @@ package freemarker.core;
import java.io.IOException;
import java.util.Collections;
import java.util.Enumeration;
+import java.util.Map;
import freemarker.template.SimpleSequence;
import freemarker.template.TemplateException;
@@ -41,11 +42,18 @@ abstract public class TemplateElement extends
TemplateObject {
private static final int INITIAL_REGULATED_CHILD_BUFFER_CAPACITY = 6;
+ // ATTENTION! If you add new fields, update #copyFieldsFrom!
+
+ /**
+ * The parent element of this element.
+ */
private TemplateElement parent;
/**
* Contains 1 or more nested elements with optional trailing {@code
null}-s, or is {@code null} exactly if there are
- * no nested elements.
+ * no nested elements. Normally, the {@link #parent} of these is the
{@code this}, however, in some exceptional
+ * cases it's not so, to avoid copying the whole descendant tree with a
different parent (as in the result of
+ * {@link Macro#Macro(Macro, Map)}.
*/
private TemplateElement[] childBuffer;
@@ -62,6 +70,8 @@ abstract public class TemplateElement extends TemplateObject {
*/
private int index;
+ // ATTENTION! If you add new fields, update #copyFieldsFrom too!
+
/**
* Executes this {@link TemplateElement}. Usually should not be called
directly, but through
* {@link Environment#visit(TemplateElement)} or a similar {@link
Environment} method.
@@ -330,6 +340,17 @@ abstract public class TemplateElement extends
TemplateObject {
this.childCount = childCount;
}
+ /**
+ * Beware, parent node of child elements won't match this element.
+ */
+ final void copyFieldsFrom(TemplateElement that) {
+ super.copyFieldsFrom(that);
+ this.parent = that.parent;
+ this.index = that.index;
+ this.childBuffer = that.childBuffer;
+ this.childCount = that.childCount;
+ }
+
final int getIndex() {
return index;
}
diff --git a/src/main/java/freemarker/core/TemplateObject.java
b/src/main/java/freemarker/core/TemplateObject.java
index 74afb1f..40f6015 100644
--- a/src/main/java/freemarker/core/TemplateObject.java
+++ b/src/main/java/freemarker/core/TemplateObject.java
@@ -33,14 +33,24 @@ import freemarker.template.Template;
*/
@Deprecated
public abstract class TemplateObject {
-
+
+ // ATTENTION! If you add new fields, update #copyFieldsFrom!
private Template template;
int beginColumn, beginLine, endColumn, endLine;
-
+ // ATTENTION! If you add new fields, update #copyFieldsFrom!
+
/** This is needed for an ?eval hack; the expression AST nodes will be the
descendants of the template, however,
* we can't give their position in the template, only in the dynamic
string that's evaluated. That's signaled
* by a negative line numbers, starting from this constant as line 1. */
- static final int RUNTIME_EVAL_LINE_DISPLACEMENT = -1000000000;
+ static final int RUNTIME_EVAL_LINE_DISPLACEMENT = -1000000000;
+
+ void copyFieldsFrom(TemplateObject that) {
+ this.template = that.template;
+ this.beginColumn = that.beginColumn;;
+ this.beginLine = that.beginLine;;
+ this.endColumn = that.endColumn;;
+ this.endLine = that.endLine;;
+ }
final void setLocation(Template template, Token begin, Token end) {
setLocation(template, begin.beginColumn, begin.beginLine,
end.endColumn, end.endLine);
diff --git a/src/main/java/freemarker/core/UnifiedCall.java
b/src/main/java/freemarker/core/UnifiedCall.java
index ccd82ec..ebed306 100644
--- a/src/main/java/freemarker/core/UnifiedCall.java
+++ b/src/main/java/freemarker/core/UnifiedCall.java
@@ -42,16 +42,17 @@ import freemarker.template.utility.StringUtil;
final class UnifiedCall extends TemplateElement implements DirectiveCallPlace {
private Expression nameExp;
- private Map namedArgs;
- private List positionalArgs, bodyParameterNames;
+ private Map<String, ? extends Expression> namedArgs;
+ private List<? extends Expression> positionalArgs;
+ private List<String> bodyParameterNames;
boolean legacySyntax;
private transient volatile
SoftReference/*List<Map.Entry<String,Expression>>*/ sortedNamedArgsCache;
private CustomDataHolder customDataHolder;
UnifiedCall(Expression nameExp,
- Map namedArgs,
+ Map<String, ? extends Expression> namedArgs,
TemplateElements children,
- List bodyParameterNames) {
+ List<String> bodyParameterNames) {
this.nameExp = nameExp;
this.namedArgs = namedArgs;
setChildren(children);
@@ -59,9 +60,9 @@ final class UnifiedCall extends TemplateElement implements
DirectiveCallPlace {
}
UnifiedCall(Expression nameExp,
- List positionalArgs,
+ List<? extends Expression> positionalArgs,
TemplateElements children,
- List bodyParameterNames) {
+ List<String> bodyParameterNames) {
this.nameExp = nameExp;
this.positionalArgs = positionalArgs;
setChildren(children);
diff --git a/src/main/java/freemarker/core/_MessageUtil.java
b/src/main/java/freemarker/core/_MessageUtil.java
index ebbac10..13f3d00 100644
--- a/src/main/java/freemarker/core/_MessageUtil.java
+++ b/src/main/java/freemarker/core/_MessageUtil.java
@@ -252,7 +252,12 @@ public class _MessageUtil {
String methodName, int argIdx, TemplateModel arg) {
return newMethodArgUnexpectedTypeException(methodName, argIdx,
"extended hash", arg);
}
-
+
+ public static TemplateModelException
newMethodArgMustBeExtendedHashOrSequnceException(
+ String methodName, int argIdx, TemplateModel arg) {
+ return newMethodArgUnexpectedTypeException(methodName, argIdx,
"extended hash or sequence", arg);
+ }
+
public static TemplateModelException newMethodArgMustBeSequenceException(
String methodName, int argIdx, TemplateModel arg) {
return newMethodArgUnexpectedTypeException(methodName, argIdx,
"sequence", arg);
diff --git a/src/main/java/freemarker/template/SimpleObjectWrapper.java
b/src/main/java/freemarker/template/SimpleObjectWrapper.java
index 39dcf08..77f3d23 100644
--- a/src/main/java/freemarker/template/SimpleObjectWrapper.java
+++ b/src/main/java/freemarker/template/SimpleObjectWrapper.java
@@ -53,13 +53,13 @@ public class SimpleObjectWrapper extends
DefaultObjectWrapper {
*/
@Override
protected TemplateModel handleUnknownType(Object obj) throws
TemplateModelException {
- throw new TemplateModelException("SimpleObjectWrapper deliberately
won't wrap this type: "
- + obj.getClass().getName());
+ throw new TemplateModelException(this.getClass().getName() + "
deliberately won't wrap this type: "
+ + obj.getClass().getName());
}
@Override
public TemplateHashModel wrapAsAPI(Object obj) throws
TemplateModelException {
- throw new TemplateModelException("SimpleObjectWrapper deliberately
doesn't allow ?api.");
+ throw new TemplateModelException(this.getClass().getName() + "
deliberately doesn't allow ?api.");
}
}
diff --git a/src/main/java/freemarker/template/TemplateDirectiveModel.java
b/src/main/java/freemarker/template/TemplateDirectiveModel.java
index 1de5867..0ac5ab6 100644
--- a/src/main/java/freemarker/template/TemplateDirectiveModel.java
+++ b/src/main/java/freemarker/template/TemplateDirectiveModel.java
@@ -69,6 +69,6 @@ public interface TemplateDirectiveModel extends TemplateModel
{
* @throws IOException When writing the template output fails. Other
{@link IOException}-s should be catched in this
* method and wrapped into {@link TemplateException}.
*/
- public void execute(Environment env, Map params, TemplateModel[] loopVars,
+ void execute(Environment env, Map params, TemplateModel[] loopVars,
TemplateDirectiveBody body) throws TemplateException, IOException;
}
\ No newline at end of file
diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml
index 502debc..2aa541d 100644
--- a/src/manual/en_US/book.xml
+++ b/src/manual/en_US/book.xml
@@ -13057,6 +13057,11 @@ grant codeBase "file:/path/to/freemarker.jar"
</listitem>
<listitem>
+ <para><link
+ linkend="ref_builtin_spread_args">spread_args</link></para>
+ </listitem>
+
+ <listitem>
<para><link linkend="ref_builtin_rtf">rtf</link></para>
</listitem>
@@ -19124,6 +19129,74 @@ Filer for positives:
a <literal>long</literal>.</para>
</section>
+ <section xml:id="ref_builtin_spread_args">
+ <title>spread_args</title>
+
+ <note>
+ <para>This built-in is available since 2.3.30</para>
+ </note>
+
+ <para>The goal of this built in is to add parameters dynamically to
+ the call of a directive (like a macro), function or method.
+ Dynamically means that parameters are added based on a hash value
+ (like <literal>{'a': 1, 'b': 2, 'c': 3}</literal>), or a sequence
+ value (like <literal>[1, 2, 3]</literal> or a Java
+ <literal>List</literal>), whose the actual content is might only
+ known when the call happens.</para>
+
+ <para>For example, we have this macro <literal>m</literal>:</para>
+
+ <programlisting role="template"><#macro m a b c>a=${a},
b=${b}, c=${c}</#macro></programlisting>
+
+ <para>Normally you call it like:</para>
+
+ <programlisting role="template"><@m a=1 b=2 c=3
/></programlisting>
+
+ <para>This calls does the same, assuming <literal>dynArgs</literal>
+ is the hash <literal>{'a': 1, 'b': 2, 'c': 3}</literal>:</para>
+
+ <programlisting role="template"><@m?spread_args(dynArgs)
/></programlisting>
+
+ <programlisting role="output">a=1, b=1, c=1</programlisting>
+
+ <para>This call also does the same, but combines dynamic arguments
+ from <literal>dynArgsAB</literal>, assumed to be <literal>{'a': 1,
+ 'b': 2}</literal>, and argument <literal>c</literal> specified
+ directly:</para>
+
+ <programlisting role="template"><@m?spread_args(dynArgsAB) c=3
/></programlisting>
+
+ <programlisting role="output">a=1, b=1, c=1</programlisting>
+
+ <para>The way if works is this.
+ <literal>m?spread_args(<replaceable>dynArgs</replaceable>)</literal>
+ returns a macro (or what <literal>m</literal> is, like function
+ etc.) whose arguments defaults to the values specified in
+ <literal><replaceable>dynArgs</replaceable></literal>. For
+ example:</para>
+
+ <programlisting role="template"><#assign mWithDefs =
m?spread_args({'b': 22, 'c': 33})>
+<@myWithDefs a=1 c='overridden'/></programlisting>
+
+ <programlisting role="output">a=1, b=22,
c=overridden</programlisting>
+
+ <para>So far we have only shown the case where the argument to
+ <literal>spread_args</literal> was a hash, which is supported for
+ all kind of directives, including macros. When this built-in is
+ applied on a function or method, the argument to
+ <literal>spread_args</literal> must be a sequence, as named
+ parameters aren't supported for those:</para>
+
+ <programlisting role="template"><#function f(a, b,
c)><#return "a=${a}, b=${b}, c=${c}"></#function>
+<#assign dynArgs=[1, 2, 3]>
+
+${f(1, 2, 3)()}
+Same as:
+${f?spread_args(dynArgs)()}</programlisting>
+
+ <para>[TODO] More details and edge cases...</para>
+ </section>
+
<section xml:id="ref_builtin_eval">
<title>eval</title>
@@ -28675,7 +28748,13 @@ TemplateModel x = env.getVariable("x"); // get
variable x</programlisting>
<itemizedlist>
<listitem>
- <para>[TODO]</para>
+ <para>Added
+
<literal>?<replaceable>spread_args</replaceable>(dynamicArguments)</literal>
+ to add parameters dynamically to directive (like macro),
+ function and method calls. Actually, this built-in returns
+ directive or macro or function that has different parameter
+ defaults. <link linkend="ref_builtin_spread_args">See more
+ here...</link></para>
</listitem>
</itemizedlist>
</section>
diff --git a/src/test/java/freemarker/core/SpreadArgsBuiltInTest.java
b/src/test/java/freemarker/core/SpreadArgsBuiltInTest.java
new file mode 100644
index 0000000..1ecc81c
--- /dev/null
+++ b/src/test/java/freemarker/core/SpreadArgsBuiltInTest.java
@@ -0,0 +1,442 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.core;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.Test;
+
+import freemarker.cache.StringTemplateLoader;
+import freemarker.template.Configuration;
+import freemarker.template.SimpleNumber;
+import freemarker.template.TemplateDirectiveBody;
+import freemarker.template.TemplateDirectiveModel;
+import freemarker.template.TemplateException;
+import freemarker.template.TemplateMethodModel;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+import freemarker.test.TemplateTest;
+
+public class SpreadArgsBuiltInTest extends TemplateTest {
+
+ private static final String PRINT_O = "o=<#if o?isSequence>[${o?join(',
')}]" +
+ "<#else>{<#list o as k,v>${k}=${v!'null'}<#sep>, </#list>}" +
+ "</#if>";
+
+ @Override
+ protected Configuration createConfiguration() throws Exception {
+ Configuration cfg = super.createConfiguration();
+ StringTemplateLoader templateLoader = new StringTemplateLoader();
+ cfg.setTemplateLoader(templateLoader);
+ templateLoader.putTemplate("callables.ftl", "" +
+ // Macro with default:
+ "<#macro m a b c='d3'>" +
+ "a=${a}; b=${b}; c=${c}" +
+ "</#macro>" +
+ // Macro with Catch-All:
+ "<#macro mCA a b o...>" +
+ "a=${a}; b=${b}; " + PRINT_O +
+ "</#macro>" +
+ // Macro with Catch-All Only:
+ "<#macro mCAO o...>" + PRINT_O +
+ "</#macro>" +
+ // Function with default:
+ "<#function f(a, b, c='d3')>" +
+ "<#return 'a=${a}; b=${b}; c=${c}'>" +
+ "</#function>" +
+ // Function with Catch-All:
+ "<#function fCA(a, b, o...)>" +
+ "<#local r>" +
+ "a=${a}; b=${b}; " + PRINT_O +
+ "</#local>" +
+ "<#return r>" +
+ "</#function>" +
+ // Function with Catch-All Only:
+ "<#function fCAO(o...)>" +
+ "<#local r>" + PRINT_O +
+ "</#local>" +
+ "<#return r>" +
+ "</#function>"
+ );
+ cfg.setAutoIncludes(Collections.singletonList("callables.ftl"));
+ return cfg;
+ }
+
+ @Test
+ public void testMacroWithNamedSpreadArgs() throws Exception {
+ assertOutput("<@m b=2 a=1 />", "a=1; b=2; c=d3");
+ assertOutput("<@m?spreadArgs({'b': 2, 'a': 1}) />", "a=1; b=2; c=d3");
+ assertOutput("<@m?spreadArgs({'b': 2, 'a': 1}) a=11 />", "a=11; b=2;
c=d3");
+ assertOutput("<@m?spreadArgs({'b': 2, 'a': 1}) a=11 b=22 />", "a=11;
b=22; c=d3");
+ assertOutput("<@m?spreadArgs({'b': 2, 'c': 3}) a=1 />", "a=1; b=2;
c=3");
+ assertOutput("<@m?spreadArgs({}) b=2 c=3 a=1 />", "a=1; b=2; c=3");
+
+ assertOutput("<@mCA a=1 b=2 />", "a=1; b=2; o={}");
+ assertOutput("<@mCA?spreadArgs({'a': 1, 'b': 2}) />", "a=1; b=2;
o={}");
+ assertOutput("<@mCA?spreadArgs({'a': 1}) b=2 />", "a=1; b=2; o={}");
+ assertOutput("<@mCA?spreadArgs({}) a=1 b=2 />", "a=1; b=2; o={}");
+ assertOutput("<@mCA?spreadArgs({'a': 1, 'b': 2, 'c': 3}) />", "a=1;
b=2; o={c=3}");
+ assertOutput("<@mCA?spreadArgs({'a': 1, 'b': 2}) c=3 />", "a=1; b=2;
o={c=3}");
+ assertOutput("<@mCA?spreadArgs({'a': 1}) b=2 c=3 />", "a=1; b=2;
o={c=3}");
+ assertOutput("<@mCA?spreadArgs({}) a=1 b=2 c=3 />", "a=1; b=2;
o={c=3}");
+ assertOutput("<@mCA a=1 b=2 c=3 />", "a=1; b=2; o={c=3}");
+ assertOutput("<@mCA a=1 b=2 c=3 d=4 />", "a=1; b=2; o={c=3, d=4}");
+ assertOutput("<@mCA?spreadArgs({'a': 1, 'b': 2, 'c': 3, 'd': 4}) />",
"a=1; b=2; o={c=3, d=4}");
+ assertOutput("<@mCA?spreadArgs({'a': 1, 'b': 2, 'c': 3, 'd': 4}) b=22
/>", "a=1; b=22; o={c=3, d=4}");
+ assertOutput("<@mCA?spreadArgs({'a': 1, 'b': 2, 'c': 3, 'd': 4}) b=22
e=5 />", "a=1; b=22; o={c=3, d=4, e=5}");
+ assertOutput("<@mCA?spreadArgs({'a': 1, 'b': 2, 'c': 3, 'd': 4}) 11 22
/>", "a=11; b=22; o={c=3, d=4}");
+ assertOutput("<@mCA?spreadArgs({'a': 1, 'b': 2}) 11 22 33 />", "a=11;
b=22; o=[33]");
+ assertErrorContains("<@mCA?spreadArgs({'a': 1, 'b': 2, 'c': 3}) 11 22
33 />",
+ "both named and positional", "catch-all");
+
+ assertOutput("<@mCAO?spreadArgs({'a': 1, 'b': 2}) />", "o={a=1, b=2}");
+ assertOutput("<@mCAO?spreadArgs({'a': 1}) b=2 />", "o={a=1, b=2}");
+ assertOutput("<@mCAO?spreadArgs({}) a=1 b=2 />", "o={a=1, b=2}");
+ assertOutput("<@mCAO a=1 b=2 />", "o={a=1, b=2}");
+
+ assertOutput("<@mCAO />", "o=[]");
+ assertOutput("<@mCAO?spreadArgs({}) />", "o={}");
+
+ assertOutput("<@m b=2 a=1 c=null />", "a=1; b=2; c=d3");
+ Map<String, Integer> cNull = new HashMap<String, Integer>();
+ cNull.put("c", null);
+ addToDataModel("cNull", cNull);
+ assertOutput("<@m?spreadArgs(cNull) b=2 a=1 />", "a=1; b=2; c=d3");
+ }
+
+ @Test
+ public void testNullsWithMacroWithNamedSpreadArgs() throws Exception {
+ // Null-s in ?spreadArgs should behave similarly as if they were given
directly as argument.
+ assertOutput("<@mCAO a=null b=null />", "o={a=null, b=null}");
+ Map<String, Integer> aNullBNull = new LinkedHashMap<String, Integer>();
+ aNullBNull.put("a", null);
+ aNullBNull.put("b", null);
+ addToDataModel("aNullBNull", aNullBNull);
+ assertOutput("<@mCAO?spreadArgs(aNullBNull) />", "o={a=null, b=null}");
+
+ assertOutput("<@m?spreadArgs({'a': 11, 'b': 22, 'c': 33}) a=111 b=222
c=null />", "a=111; b=222; c=d3");
+ assertErrorContains("<@m?spreadArgs({'a': 11, 'b': 22, 'c': 33}) a=111
b=null c=333 />", "required", "\"b\"");
+ assertOutput("<@mCAO?spreadArgs({'a': 1, 'b': 2}) a=null b=22 c=33
/>", "o={a=null, b=22, c=33}");
+ }
+
+ @Test
+ public void testMacroWithPositionalSpreadArgs() throws Exception {
+ assertOutput("<@m 1 2 />", "a=1; b=2; c=d3");
+ assertOutput("<@m?spreadArgs([1, 2]) />", "a=1; b=2; c=d3");
+ assertOutput("<@m?spreadArgs([1]) 2 />", "a=1; b=2; c=d3");
+ assertOutput("<@m?spreadArgs([]) 1 2 />", "a=1; b=2; c=d3");
+ assertOutput("<@m 1 2 3 />", "a=1; b=2; c=3");
+ assertOutput("<@m?spreadArgs([1, 2, 3]) />", "a=1; b=2; c=3");
+ assertOutput("<@m?spreadArgs([1, 2]) c=3 />", "a=1; b=2; c=3");
+ assertOutput("<@m?spreadArgs([1, 2, 0]) c=3 />", "a=1; b=2; c=3");
+ assertOutput("<@m?spreadArgs([1, 0, 3]) b=2 />", "a=1; b=2; c=3");
+
+ assertOutput("<@mCA 1 2 />", "a=1; b=2; o=[]");
+ assertOutput("<@mCA?spreadArgs([1, 2]) />", "a=1; b=2; o=[]");
+ assertOutput("<@mCA?spreadArgs([1]) 2 />", "a=1; b=2; o=[]");
+ assertOutput("<@mCA?spreadArgs([]) 1 2 />", "a=1; b=2; o=[]");
+ assertOutput("<@mCA 1 2 3 />", "a=1; b=2; o=[3]");
+ assertOutput("<@mCA?spreadArgs([1, 2, 3]) />", "a=1; b=2; o=[3]");
+ assertOutput("<@mCA?spreadArgs([1]) 2, 3 />", "a=1; b=2; o=[3]");
+ assertOutput("<@mCA?spreadArgs([1, 2]) 3 />", "a=1; b=2; o=[3]");
+ assertOutput("<@mCA?spreadArgs([1]) b=2 c=3 />", "a=1; b=2; o={c=3}");
+ assertOutput("<@mCA?spreadArgs([]) a=1 b=2 c=3 />", "a=1; b=2;
o={c=3}");
+ assertOutput("<@mCA?spreadArgs([1, 2]) c=3 />", "a=1; b=2; o={c=3}");
+ assertOutput("<@mCA?spreadArgs([1, 0]) b=2 c=3 />", "a=1; b=2;
o={c=3}");
+ assertErrorContains("<@mCA?spreadArgs([1, 2, 3]) d=4 />",
+ "both named and positional", "catch-all");
+
+ assertOutput("<@mCAO?spreadArgs([1, 2]) />", "o=[1, 2]");
+ assertOutput("<@mCAO?spreadArgs([1]) 2 />", "o=[1, 2]");
+ assertOutput("<@mCAO 1, 2 />", "o=[1, 2]");
+
+ assertOutput("<@mCAO?spreadArgs([]) />", "o=[]");
+ }
+
+ @Test
+ public void testNullsWithMacroWithPositionalSpreadArgs() throws Exception {
+ // Null-s in ?spreadArgs should behave similarly as if they were given
directly as argument.
+ assertOutput("<@mCAO 1 null null 4 />", "o=[1, 4]"); // [FM3] Should
be: 1, null, null, 4
+ addToDataModel("args", Arrays.asList(1, null, null, 4));
+ assertOutput("<@mCAO?spreadArgs(args) />", "o=[1, 4]"); // [FM3] See
above
+ assertOutput("<@mCAO?spreadArgs(args) null 5 6 />", "o=[1, 4, 5, 6]");
// [FM3] See above
+ }
+
+ @Test
+ public void testFunction() throws Exception {
+ assertOutput("${f(1, 2)}", "a=1; b=2; c=d3");
+ assertOutput("${f?spreadArgs([1, 2])()}", "a=1; b=2; c=d3");
+ assertOutput("${f?spreadArgs([1])(2)}", "a=1; b=2; c=d3");
+ assertOutput("${f?spreadArgs([])(1, 2)}", "a=1; b=2; c=d3");
+ assertOutput("${f(1, 2, 3)}", "a=1; b=2; c=3");
+ assertOutput("${f?spreadArgs([1, 2, 3])()}", "a=1; b=2; c=3");
+
+ assertOutput("${fCA(1, 2)}", "a=1; b=2; o=[]");
+ assertOutput("${fCA?spreadArgs([1, 2])()}", "a=1; b=2; o=[]");
+ assertOutput("${fCA?spreadArgs([1])(2)}", "a=1; b=2; o=[]");
+ assertOutput("${fCA?spreadArgs([])(1, 2)}", "a=1; b=2; o=[]");
+ assertOutput("${fCA(1, 2, 3)}", "a=1; b=2; o=[3]");
+ assertOutput("${fCA?spreadArgs([1, 2, 3])()}", "a=1; b=2; o=[3]");
+ assertOutput("${fCA?spreadArgs([1])(2, 3)}", "a=1; b=2; o=[3]");
+ assertOutput("${fCA?spreadArgs([1, 2])(3)}", "a=1; b=2; o=[3]");
+ assertOutput("${fCA?spreadArgs([])(1, 2, 3)}", "a=1; b=2; o=[3]");
+
+ assertOutput("${fCAO(1, 2)}", "o=[1, 2]");
+ assertOutput("${fCAO?spreadArgs([1, 2])()}", "o=[1, 2]");
+ assertOutput("${fCAO?spreadArgs([1])(2)}", "o=[1, 2]");
+ assertOutput("${fCAO?spreadArgs([])(1, 2)}", "o=[1, 2]");
+
+ assertErrorContains("${f?spreadArgs({'a': 1, 'b': 2})}",
+ "function", "hash", "sequence", "?spreadArgs");
+ }
+
+ @Test
+ public void testNullsWithFunction() throws Exception {
+ // Null-s in ?spreadArgs should behave similarly as if they were given
directly as argument.
+ assertOutput("${fCAO(1, null, null, 4)}", "o=[1, 4]"); // [FM3] Should
be: 1, null, null, 4
+ addToDataModel("args", Arrays.asList(1, null, null, 4));
+ assertOutput("${fCAO?spreadArgs(args)()}", "o=[1, 4]"); // [FM3] See
above
+ assertOutput("${fCAO?spreadArgs(args)(null, 5, 6)}", "o=[1, 4, 5,
6]"); // [FM3] See above
+ }
+
+ @Test
+ public void testCurrentNamespaceWorks() throws Exception {
+ addTemplate("ns1.ftl", "" +
+ "<#assign v = 'NS1'>" +
+ "<#macro m p>" +
+ "p=${p} " +
+ "v=${v} " +
+ "<#local v = 'L'>" +
+ "v=${v} " +
+ "{<#nested p>} " +
+ "v=${v}" +
+ "</#macro>");
+ assertOutput("" +
+ "<#import 'ns1.ftl' as ns1>" +
+ "<#assign v = 'NS0'>" +
+ "<@ns1.m 1; n>n=${n} v=${v}</@>; " +
+ "<#assign m2 = ns1.m?spreadArgs([2])>" +
+ "<@m2; n>n=${n} v=${v}</@>",
+ "p=1 v=NS1 v=L {n=1 v=NS0} v=L; " +
+ "p=2 v=NS1 v=L {n=2 v=NS0} v=L");
+ }
+
+ @Test
+ public void testArgCountCheck() throws Exception {
+ String macroDef = "<#macro m a b c>${a}, ${b}, ${c}</#macro>";
+
+ // No error:
+ assertOutput(macroDef + "<@m 1 2 3 />", "1, 2, 3");
+ assertOutput(macroDef + "<@m?spread_args([1, 2, 3]) />", "1, 2, 3");
+ assertOutput(macroDef + "<@m?spread_args([1, 2]) 3 />", "1, 2, 3");
+
+ // Too many args:
+ assertErrorContains(macroDef + "<@m 1 2 3 4 />", "accepts 3", "got 4");
+ assertErrorContains(macroDef + "<@m?spread_args([1, 2, 3, 4]) />",
"accepts 3", "got 4");
+ assertErrorContains(macroDef + "<@m?spread_args([1, 2, 3]) 5 />",
"accepts 3", "got 4");
+ assertErrorContains(macroDef + "<@m?spread_args([1]) 2 3 4 />",
"accepts 3", "got 4");
+
+ // Too few args:
+ assertErrorContains(macroDef + "<@m 1 2 />", "\"c\"", "was not
specified");
+ assertErrorContains(macroDef + "<@m?spread_args([1, 2]) />", "\"c\"",
"was not specified");
+ assertErrorContains(macroDef + "<@m?spread_args([1]) 2 />", "\"c\"",
"was not specified");
+ assertErrorContains(macroDef + "<@m?spread_args([]) 1 2 />", "\"c\"",
"was not specified");
+ }
+
+ @Test
+ public void testDefaultsThenCatchAll() throws IOException,
TemplateException {
+ String macroDef = "<#macro m a=1 b=2 c=3 o...>a=${a} b=${b} c=${c} " +
PRINT_O + "</#macro>";
+
+ assertOutput(macroDef + "<@m?spreadArgs([]) />", "a=1 b=2 c=3 o=[]");
+ assertOutput(macroDef + "<@m?spreadArgs([11]) />", "a=11 b=2 c=3
o=[]");
+ assertOutput(macroDef + "<@m?spreadArgs([11, 22]) />", "a=11 b=22 c=3
o=[]");
+ assertOutput(macroDef + "<@m?spreadArgs([11, 22, 33]) />", "a=11 b=22
c=33 o=[]");
+ assertOutput(macroDef + "<@m?spreadArgs([11, 22, 33, 44]) />", "a=11
b=22 c=33 o=[44]");
+ assertOutput(macroDef + "<@m?spreadArgs([11, 22, 33, 44, 55]) />",
"a=11 b=22 c=33 o=[44, 55]");
+
+ assertOutput(macroDef + "<@m?spreadArgs([]) 11 />", "a=11 b=2 c=3
o=[]");
+ assertOutput(macroDef + "<@m?spreadArgs([11]) 22 />", "a=11 b=22 c=3
o=[]");
+ assertOutput(macroDef + "<@m?spreadArgs([11, 22]) 33 />", "a=11 b=22
c=33 o=[]");
+ assertOutput(macroDef + "<@m?spreadArgs([11, 22, 33]) 44 />", "a=11
b=22 c=33 o=[44]");
+ assertOutput(macroDef + "<@m?spreadArgs([11, 22, 33, 44]) 55 />",
"a=11 b=22 c=33 o=[44, 55]");
+
+ assertOutput(macroDef + "<@m?spreadArgs({}) />", "a=1 b=2 c=3 o={}");
+ assertOutput(macroDef + "<@m?spreadArgs({'b':22}) />", "a=1 b=22 c=3
o={}");
+ assertOutput(macroDef + "<@m?spreadArgs({'b':22, 'c':33}) />", "a=1
b=22 c=33 o={}");
+ assertOutput(macroDef + "<@m?spreadArgs({'b':22, 'c':33, 'd':55}) />",
"a=1 b=22 c=33 o={d=55}");
+ assertOutput(macroDef + "<@m?spreadArgs({'b':22, 'd':55, 'e':66}) />",
"a=1 b=22 c=3 o={d=55, e=66}");
+
+ assertOutput(macroDef + "<@m?spreadArgs({}) b=22 />", "a=1 b=22 c=3
o={}");
+ assertOutput(macroDef + "<@m?spreadArgs({'b':22}) c=33 />", "a=1 b=22
c=33 o={}");
+ assertOutput(macroDef + "<@m?spreadArgs({'b':22, 'c':33}) d=55 />",
"a=1 b=22 c=33 o={d=55}");
+ assertOutput(macroDef + "<@m?spreadArgs({'b':22, 'd':55}) e=66 />",
"a=1 b=22 c=3 o={d=55, e=66}");
+ }
+
+ @Test
+ public void testMethod() throws IOException, TemplateException {
+ addToDataModel("obj", new MethodHolder());
+
+ assertOutput("${obj.m3p(1, 2, 3)}", "1, 2, 3");
+ assertOutput("${obj.m3p?spreadArgs([1, 2, 3])()}", "1, 2, 3");
+ assertOutput("${obj.m3p?spreadArgs([1, 2])(3)}", "1, 2, 3");
+ assertOutput("${obj.m3p?spreadArgs([1])(2, 3)}", "1, 2, 3");
+ assertOutput("${obj.m3p?spreadArgs([])(1, 2, 3)}", "1, 2, 3");
+
+ assertOutput("${obj.m0p()}", "OK");
+ assertOutput("${obj.m0p?spreadArgs([])()}", "OK");
+
+ assertOutput("${obj.mVA(1, 2, 3, 4)}", "1, 2, o=[3, 4]");
+ assertOutput("${obj.mVA?spreadArgs([1, 2, 3, 4])()}", "1, 2, o=[3,
4]");
+ assertOutput("${obj.mVA?spreadArgs([1, 2, 3])(4)}", "1, 2, o=[3, 4]");
+ assertOutput("${obj.mVA?spreadArgs([1, 2])(3, 4)}", "1, 2, o=[3, 4]");
+ assertOutput("${obj.mVA?spreadArgs([1])(2, 3, 4)}", "1, 2, o=[3, 4]");
+ assertOutput("${obj.mVA?spreadArgs([])(1, 2, 3, 4)}", "1, 2, o=[3,
4]");
+
+ assertErrorContains("${obj.mVA?spreadArgs({})}", "hash", "sequence",
"argument");
+
+ assertOutput("${obj.mNullable(null, 2, null)}", "null, 2, null");
+ addToDataModel("args", Arrays.asList(null, 2, null));
+ assertOutput("${obj.mNullable?spreadArgs(args)()}", "null, 2, null");
+ }
+
+ public static class MethodHolder {
+ public String m3p(int a, int b, int c) {
+ return a + ", " + b + ", " + c;
+ }
+
+ public String m0p() {
+ return "OK";
+ }
+
+ public String mVA(int a, int b, int... others) {
+ StringBuilder sb = new StringBuilder()
+ .append(a).append(", ").append(b);
+ sb.append(", o=[");
+ for (int i = 0; i < others.length; i++) {
+ if (i > 0) {
+ sb.append(", ");
+ }
+ sb.append(others[i]);
+ }
+ sb.append("]");
+ return sb.toString();
+ }
+
+ public String mNullable(Integer a, Integer b, Integer c) {
+ return a + ", " + b + ", " + c;
+ }
+ }
+
+ @Test
+ public void testLegacyMethod() throws IOException, TemplateException {
+ addToDataModel("legacyMethod", new LegacyMethodModel());
+ getConfiguration().setNumberFormat("0.00");
+ assertOutput("${legacyMethod(1, '2')}", "[1.00, 2]");
+ assertOutput("${legacyMethod?spreadArgs([1, '2'])()}", "[1.00, 2]");
+ assertOutput("${legacyMethod?spreadArgs([1])('2')}", "[1.00, 2]");
+ assertOutput("${legacyMethod?spreadArgs([])(1, '2')}", "[1.00, 2]");
+ }
+
+ private static class LegacyMethodModel implements TemplateMethodModel {
+ public Object exec(List arguments) throws TemplateModelException {
+ for (Object argument : arguments) {
+ if (!(argument instanceof String)) {
+ throw new IllegalArgumentException("Arguments should be
String-s");
+ }
+ }
+ return arguments.toString();
+ }
+ }
+
+ @Test
+ public void testTemplateDirectiveModel() throws IOException,
TemplateException {
+ addToDataModel("directive", new TestTemplateDirectiveModel());
+
+ assertOutput("<@directive a=1 b=2 c=3; u, v>${u} ${v}</@>",
+ "{a=1, b=2, c=3}{11 22}");
+ assertOutput("<@directive?spreadArgs({'a': 1, 'b': 2, 'c': 3}); u,
v>${u} ${v}</@>",
+ "{a=1, b=2, c=3}{11 22}");
+ assertOutput("<@directive?spreadArgs({'a': 1, 'b': 2}) c=3; u, v>${u}
${v}</@>",
+ "{a=1, b=2, c=3}{11 22}");
+ assertOutput("<@directive?spreadArgs({'a': 1}) b=2 c=3; u, v>${u}
${v}</@>",
+ "{a=1, b=2, c=3}{11 22}");
+ assertOutput("<@directive?spreadArgs({}) a=1 b=2 c=3; u, v>${u}
${v}</@>",
+ "{a=1, b=2, c=3}{11 22}");
+
+ assertOutput("<@directive?spreadArgs({}); u, v>${u} ${v}</@>",
+ "{}{11 22}");
+ assertOutput("<@directive?spreadArgs({'a': 1, 'b': 2}) b=22 c=3;
u>${u}</@>",
+ "{a=1, b=22, c=3}{11}");
+ Map<String, Integer> args = new LinkedHashMap<String, Integer>();
+ args.put("a", null);
+ args.put("b", 2);
+ args.put("c", 3);
+ args.put("e", 6);
+ addToDataModel("args", args);
+ assertOutput("<@directive?spreadArgs(args) b=22 c=null d=55 />",
+ "{a=null, b=22, c=null, e=6, d=55}{}");
+ }
+
+ private static class TestTemplateDirectiveModel implements
TemplateDirectiveModel {
+
+ public void execute(Environment env, Map params, TemplateModel[]
loopVars, TemplateDirectiveBody body) throws
+ TemplateException, IOException {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{");
+ boolean first = true;
+ for (Map.Entry<String, TemplateModel> param : ((Map<String,
TemplateModel>) params).entrySet()) {
+ if (!first) {
+ sb.append(", ");
+ } else {
+ first = false;
+ }
+ sb.append(param.getKey());
+ sb.append("=");
+ TemplateModel value = param.getValue();
+ sb.append(value != null ?
EvalUtil.coerceModelToPlainText(value, null, null, env) : "null");
+ }
+ sb.append("}");
+ env.getOut().write(sb.toString());
+
+ if (loopVars.length > 0) {
+ loopVars[0] = new SimpleNumber(11);
+ if (loopVars.length > 1) {
+ loopVars[1] = new SimpleNumber(22);
+ if (loopVars.length > 2) {
+ throw new TemplateModelException("Too many loop vars");
+ }
+ }
+ }
+
+ env.getOut().write("{");
+ if (body != null) {
+ body.render(env.getOut());
+ }
+ env.getOut().write("}");
+ }
+ }
+
+}
\ No newline at end of file