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">&lt;#macro m a b c&gt;a=${a}, 
b=${b}, c=${c}&lt;/#macro&gt;</programlisting>
+
+          <para>Normally you call it like:</para>
+
+          <programlisting role="template">&lt;@m a=1 b=2 c=3 
/&gt;</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">&lt;@m?spread_args(dynArgs) 
/&gt;</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">&lt;@m?spread_args(dynArgsAB) c=3 
/&gt;</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">&lt;#assign mWithDefs = 
m?spread_args({'b': 22, 'c': 33})&gt;
+&lt;@myWithDefs a=1 c='overridden'/&gt;</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">&lt;#function f(a, b, 
c)&gt;&lt;#return "a=${a}, b=${b}, c=${c}"&gt;&lt;/#function&gt;
+&lt;#assign dynArgs=[1, 2, 3]&gt;
+
+${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

Reply via email to