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 6c45eb6 Added ?with_args_last(args). Also some cleanup in code related to ?with_args. 6c45eb6 is described below commit 6c45eb6aebc659bf38913caed747a453c72f2a8a Author: ddekany <ddek...@apache.org> AuthorDate: Sat Nov 16 22:53:31 2019 +0100 Added ?with_args_last(args). Also some cleanup in code related to ?with_args. --- src/main/java/freemarker/core/BuiltIn.java | 9 +- .../java/freemarker/core/BuiltInsForCallables.java | 81 +++++++--- src/main/java/freemarker/core/Environment.java | 165 +++++++++++++++------ src/main/java/freemarker/core/Macro.java | 25 +++- src/manual/en_US/book.xml | 155 ++++++++++++++++++- .../java/freemarker/core/WithArgsBuiltInTest.java | 147 +++++++++++++++++- .../freemarker/manual/WithArgsLastExamples.java | 35 +++++ .../freemarker/manual/WithArgsLastExamples.ftl | 25 ++++ 8 files changed, 564 insertions(+), 78 deletions(-) diff --git a/src/main/java/freemarker/core/BuiltIn.java b/src/main/java/freemarker/core/BuiltIn.java index da9ffc6..36e74b7 100644 --- a/src/main/java/freemarker/core/BuiltIn.java +++ b/src/main/java/freemarker/core/BuiltIn.java @@ -84,11 +84,13 @@ 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 = 287; + static final int NUMBER_OF_BIS = 289; 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_WITH_ARGS = "with_args"; static final String BI_NAME_CAMEL_CASE_WITH_ARGS = "withArgs"; + static final String BI_NAME_SNAKE_CASE_WITH_ARGS_LAST = "with_args_last"; + static final String BI_NAME_CAMEL_CASE_WITH_ARGS_LAST = "withArgsLast"; static { // Note that you must update NUMBER_OF_BIS if you add new items here! @@ -300,7 +302,10 @@ abstract class BuiltIn extends Expression implements Cloneable { putBI("url_path", "urlPath", new BuiltInsForStringsEncoding.urlPathBI()); putBI("values", new BuiltInsForHashes.valuesBI()); putBI("web_safe", "webSafe", BUILT_INS_BY_NAME.get("html")); // deprecated; use ?html instead - putBI(BI_NAME_SNAKE_CASE_WITH_ARGS, BI_NAME_CAMEL_CASE_WITH_ARGS, new BuiltInsForCallables.with_argsBI()); + putBI(BI_NAME_SNAKE_CASE_WITH_ARGS, BI_NAME_CAMEL_CASE_WITH_ARGS, + new BuiltInsForCallables.with_argsBI()); + putBI(BI_NAME_SNAKE_CASE_WITH_ARGS_LAST, BI_NAME_CAMEL_CASE_WITH_ARGS_LAST, + new BuiltInsForCallables.with_args_lastBI()); putBI("word_list", "wordList", new BuiltInsForStringsBasic.word_listBI()); putBI("xhtml", new BuiltInsForStringsEncoding.xhtmlBI()); putBI("xml", new BuiltInsForStringsEncoding.xmlBI()); diff --git a/src/main/java/freemarker/core/BuiltInsForCallables.java b/src/main/java/freemarker/core/BuiltInsForCallables.java index acc4245..87d6647 100644 --- a/src/main/java/freemarker/core/BuiltInsForCallables.java +++ b/src/main/java/freemarker/core/BuiltInsForCallables.java @@ -40,7 +40,9 @@ import freemarker.template.utility.TemplateModelUtils; class BuiltInsForCallables { - static class with_argsBI extends BuiltIn { + static abstract class AbstractWithArgsBI extends BuiltIn { + + protected abstract boolean isOrderLast(); TemplateModel _eval(Environment env) throws TemplateException { TemplateModel model = target.eval(env); @@ -73,13 +75,13 @@ class BuiltInsForCallables { Macro.WithArgs withArgs; if (argTM instanceof TemplateSequenceModel) { - withArgs = new Macro.WithArgs((TemplateSequenceModel) argTM); + withArgs = new Macro.WithArgs((TemplateSequenceModel) argTM, isOrderLast()); } 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."); } - withArgs = new Macro.WithArgs((TemplateHashModelEx) argTM); + withArgs = new Macro.WithArgs((TemplateHashModelEx) argTM, isOrderLast()); } else { throw _MessageUtil.newMethodArgMustBeExtendedHashOrSequnceException("?" + key, 0, argTM); } @@ -110,11 +112,15 @@ class BuiltInsForCallables { List<TemplateModel> newArgs = new ArrayList<TemplateModel>( withArgsSize + origArgs.size()); + if (isOrderLast()) { + newArgs.addAll(origArgs); + } for (int i = 0; i < withArgsSize; i++) { newArgs.add(withArgs.get(i)); } - - newArgs.addAll(origArgs); + if (!isOrderLast()) { + newArgs.addAll(origArgs); + } return method.exec(newArgs); } @@ -126,12 +132,16 @@ class BuiltInsForCallables { List<String> newArgs = new ArrayList<String>( withArgsSize + origArgs.size()); + if (isOrderLast()) { + newArgs.addAll(origArgs); + } for (int i = 0; i < withArgsSize; i++) { TemplateModel argVal = withArgs.get(i); newArgs.add(argValueToString(argVal)); } - - newArgs.addAll(origArgs); + if (!isOrderLast()) { + newArgs.addAll(origArgs); + } return method.exec(newArgs); } @@ -187,33 +197,48 @@ class BuiltInsForCallables { public void execute(Environment env, Map origArgs, TemplateModel[] loopVars, TemplateDirectiveBody body) throws TemplateException, IOException { int withArgsSize = withArgs.size(); + // This is unnecessarily big if there are overridden arguments, but we care more about + // avoiding rehashing. Map<String, TemplateModel> newArgs = new LinkedHashMap<String, TemplateModel>( (withArgsSize + origArgs.size()) * 4 / 3, 1f); TemplateHashModelEx2.KeyValuePairIterator withArgsIter = TemplateModelUtils.getKeyValuePairIterator(withArgs); - while (withArgsIter.hasNext()) { - TemplateHashModelEx2.KeyValuePair spreadArgKVP = withArgsIter.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)), "."); + if (isOrderLast()) { + newArgs.putAll(origArgs); + while (withArgsIter.hasNext()) { + TemplateHashModelEx2.KeyValuePair withArgsKVP = withArgsIter.next(); + String argName = getArgumentName(withArgsKVP); + if (!newArgs.containsKey(argName)) { + newArgs.put(argName, withArgsKVP.getValue()); + } } - String argName = EvalUtil.modelToString((TemplateScalarModel) argNameTM, null, null); - - newArgs.put(argName, spreadArgKVP.getValue()); + } else { + while (withArgsIter.hasNext()) { + TemplateHashModelEx2.KeyValuePair withArgsKVP = withArgsIter.next(); + newArgs.put(getArgumentName(withArgsKVP), withArgsKVP.getValue()); + } + newArgs.putAll(origArgs); } - newArgs.putAll(origArgs); // TODO Should null replace non-null? - directive.execute(env, newArgs, loopVars, body); } + + private String getArgumentName(TemplateHashModelEx2.KeyValuePair withArgsKVP) throws + TemplateModelException { + TemplateModel argNameTM = withArgsKVP.getKey(); + if (!(argNameTM instanceof TemplateScalarModel)) { + throw new _TemplateModelException( + "Expected string keys in the ?", key, "(...) arguments, " + + "but one of the keys was ", + new _DelayedAOrAn(new _DelayedFTLTypeDescription(argNameTM)), "."); + } + return EvalUtil.modelToString((TemplateScalarModel) argNameTM, null, null); + } }; } else if (argTM instanceof TemplateSequenceModel) { throw new _TemplateModelException("When applied on a directive, ?", key, - " can't have a sequence argument. Use a hash argument."); + "(...) can't have a sequence argument. Use a hash argument."); } else { throw _MessageUtil.newMethodArgMustBeExtendedHashOrSequnceException("?" + key, 0, argTM); } @@ -223,4 +248,18 @@ class BuiltInsForCallables { } + static final class with_argsBI extends AbstractWithArgsBI { + @Override + protected boolean isOrderLast() { + return false; + } + } + + static final class with_args_lastBI extends AbstractWithArgsBI { + @Override + protected boolean isOrderLast() { + return true; + } + } + } diff --git a/src/main/java/freemarker/core/Environment.java b/src/main/java/freemarker/core/Environment.java index 8ca29b1..fcce497 100644 --- a/src/main/java/freemarker/core/Environment.java +++ b/src/main/java/freemarker/core/Environment.java @@ -904,32 +904,31 @@ public final class Environment extends Configurable { int nextPositionalArgToAssignIdx = 0; // Used for ?with_args(...): - Macro.WithArgs withArgs = macro.getWithArgs(); - if (withArgs != null) { - TemplateHashModelEx byNameWithArgs = withArgs.getByName(); - TemplateSequenceModel byPositionWithArgs = withArgs.getByPosition(); + WithArgsState withArgsState = getWithArgState(macro); + if (withArgsState != null) { + TemplateHashModelEx byNameWithArgs = withArgsState.byName; + TemplateSequenceModel byPositionWithArgs = withArgsState.byPosition; if (byNameWithArgs != null) { - new HashMap<String, TemplateModel>(byNameWithArgs.size() * 4 / 3, 1f); - TemplateHashModelEx2.KeyValuePairIterator namedParamValueOverridesIter = - TemplateModelUtils.getKeyValuePairIterator(byNameWithArgs); - while (namedParamValueOverridesIter.hasNext()) { - TemplateHashModelEx2.KeyValuePair defaultArgHashKVP = namedParamValueOverridesIter.next(); + TemplateHashModelEx2.KeyValuePairIterator withArgsKVPIter + = TemplateModelUtils.getKeyValuePairIterator(byNameWithArgs); + while (withArgsKVPIter.hasNext()) { + TemplateHashModelEx2.KeyValuePair withArgKVP = withArgsKVPIter.next(); String argName; { - TemplateModel argNameTM = defaultArgHashKVP.getKey(); + TemplateModel argNameTM = withArgKVP.getKey(); if (!(argNameTM instanceof TemplateScalarModel)) { throw new _TemplateModelException( - "Expected string keys in the spread args hash, but one of the keys was ", + "Expected string keys in the \"with args\" hash, but one of the keys was ", new _DelayedAOrAn(new _DelayedFTLTypeDescription(argNameTM)), "."); } argName = EvalUtil.modelToString((TemplateScalarModel) argNameTM, null, null); } - TemplateModel argValue = defaultArgHashKVP.getValue(); + TemplateModel argValue = withArgKVP.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. + // to <@macroWithCatchAll a=null b=null />, which 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) { @@ -938,39 +937,67 @@ public final class Environment extends Configurable { if (namedCatchAllParamValue == null) { namedCatchAllParamValue = initNamedCatchAllParameter(macroCtx, catchAllParamName); } - namedCatchAllParamValue.put(argName, argValue); + if (!withArgsState.orderLast) { + namedCatchAllParamValue.put(argName, argValue); + } else { + List<NameValuePair> orderLastByNameCatchAll = withArgsState.orderLastByNameCatchAll; + if (orderLastByNameCatchAll == null) { + orderLastByNameCatchAll = new ArrayList<NameValuePair>(); + withArgsState.orderLastByNameCatchAll = orderLastByNameCatchAll; + } + orderLastByNameCatchAll.add(new NameValuePair(argName, argValue)); + } } else { throw newUndeclaredParamNameException(macro, argName); } - } + } // while (withArgsKVPIter.hasNext()) } else if (byPositionWithArgs != null) { - String[] argNames = macro.getArgumentNamesInternal(); - final int argsCnt = byPositionWithArgs.size(); - if (argNames.length < argsCnt && catchAllParamName == null) { - throw newTooManyArgumentsException(macro, argNames, argsCnt); - } - for (int i = 0; i < argsCnt; i++) { - TemplateModel argValue = byPositionWithArgs.get(i); - try { - if (nextPositionalArgToAssignIdx < argNames.length) { - String argName = argNames[nextPositionalArgToAssignIdx++]; - macroCtx.setLocalVar(argName, argValue); - } else { - if (positionalCatchAllParamValue == null) { - positionalCatchAllParamValue = initPositionalCatchAllParameter(macroCtx, catchAllParamName); + if (!withArgsState.orderLast) { // ?withArgs + String[] argNames = macro.getArgumentNamesNoCopy(); + final int argsCnt = byPositionWithArgs.size(); + if (argNames.length < argsCnt && catchAllParamName == null) { + throw newTooManyArgumentsException(macro, argNames, argsCnt); + } + for (int argIdx = 0; argIdx < argsCnt; argIdx++) { + TemplateModel argValue = byPositionWithArgs.get(argIdx); + try { + if (nextPositionalArgToAssignIdx < argNames.length) { + String argName = argNames[nextPositionalArgToAssignIdx++]; + macroCtx.setLocalVar(argName, argValue); + } else { + if (positionalCatchAllParamValue == null) { + positionalCatchAllParamValue = initPositionalCatchAllParameter(macroCtx, catchAllParamName); + } + positionalCatchAllParamValue.add(argValue); } - positionalCatchAllParamValue.add(argValue); + } catch (RuntimeException re) { + throw new _MiscTemplateException(re, this); + } + } + } else { // ?withArgsLast + if (namedArgs != null && !namedArgs.isEmpty() && byPositionWithArgs.size() != 0) { + // Unlike with ?withArgs, here we can't know in general which argument byPositionWithArgs[0] + // meant to refer to, as the named arguments have already taken some indexes. + throw new _MiscTemplateException("Call can't pass parameters by name, as there's " + + "\"with args last\" in effect that specifies parameters by position."); + } + if (catchAllParamName == null) { + // To fail before Expression-s for some normal arguments are evaluated: + int totalPositionalArgCnt = + (positionalArgs != null ? positionalArgs.size() : 0) + byPositionWithArgs.size(); + if (totalPositionalArgCnt > macro.getArgumentNamesNoCopy().length) { + throw newTooManyArgumentsException(macro, macro.getArgumentNamesNoCopy(), totalPositionalArgCnt); } - } catch (RuntimeException re) { - throw new _MiscTemplateException(re, this); } } } - } + } // if (withArgsState != null) if (namedArgs != null) { if (catchAllParamName != null && namedCatchAllParamValue == null && positionalCatchAllParamValue == null) { - if (namedArgs.isEmpty() && withArgs != null && withArgs.getByPosition() != null) { + // If a macro call has no argument (like <@m />), before 2.3.30 we assumed it's a by-name call. But now + // if we have ?with_args(args), its argument type decides if the call is by-name or by-position. + if (namedArgs.isEmpty() && withArgsState != null && withArgsState.byPosition != null) { positionalCatchAllParamValue = initPositionalCatchAllParameter(macroCtx, catchAllParamName); } else { namedCatchAllParamValue = initNamedCatchAllParameter(macroCtx, catchAllParamName); @@ -998,14 +1025,14 @@ public final class Environment extends Configurable { } } else if (positionalArgs != null) { if (catchAllParamName != null && positionalCatchAllParamValue == null && namedCatchAllParamValue == null) { - if (positionalArgs.isEmpty() && withArgs != null && withArgs.getByName() != null) { + if (positionalArgs.isEmpty() && withArgsState != null && withArgsState.byName != null) { namedCatchAllParamValue = initNamedCatchAllParameter(macroCtx, catchAllParamName); } else { positionalCatchAllParamValue = initPositionalCatchAllParameter(macroCtx, catchAllParamName); } } - String[] argNames = macro.getArgumentNamesInternal(); + String[] argNames = macro.getArgumentNamesNoCopy(); final int argsCnt = positionalArgs.size(); final int argsWithWithArgsCnt = argsCnt + nextPositionalArgToAssignIdx; if (argNames.length < argsWithWithArgsCnt && positionalCatchAllParamValue == null) { @@ -1017,21 +1044,75 @@ public final class Environment extends Configurable { } for (int srcPosArgIdx = 0; srcPosArgIdx < argsCnt; srcPosArgIdx++) { Expression argValueExp = positionalArgs.get(srcPosArgIdx); - TemplateModel argValue = argValueExp.eval(this); + TemplateModel argValue; try { + argValue = argValueExp.eval(this); + } catch (RuntimeException e) { + throw new _MiscTemplateException(e, this); + } + if (nextPositionalArgToAssignIdx < argNames.length) { + String argName = argNames[nextPositionalArgToAssignIdx++]; + macroCtx.setLocalVar(argName, argValue); + } else { + positionalCatchAllParamValue.add(argValue); + } + } + } // else if (positionalArgs != null) + + if (withArgsState != null && withArgsState.orderLast) { + if (withArgsState.orderLastByNameCatchAll != null) { + for (NameValuePair nameValuePair : withArgsState.orderLastByNameCatchAll) { + if (!namedCatchAllParamValue.containsKey(nameValuePair.name)) { + namedCatchAllParamValue.put(nameValuePair.name, nameValuePair.value); + } + } + } else if (withArgsState.byPosition != null) { + TemplateSequenceModel byPosition = withArgsState.byPosition; + int withArgCnt = byPosition.size(); + String[] argNames = macro.getArgumentNamesNoCopy(); + for (int withArgIdx = 0; withArgIdx < withArgCnt; withArgIdx++) { + TemplateModel withArgValue = byPosition.get(withArgIdx); if (nextPositionalArgToAssignIdx < argNames.length) { String argName = argNames[nextPositionalArgToAssignIdx++]; - macroCtx.setLocalVar(argName, argValue); + macroCtx.setLocalVar(argName, withArgValue); } else { - positionalCatchAllParamValue.add(argValue); + // It was checked much earlier that we don't have too many arguments, so this must work: + positionalCatchAllParamValue.add(withArgValue); } - } catch (RuntimeException re) { - throw new _MiscTemplateException(re, this); } } } } + private static WithArgsState getWithArgState(Macro macro) { + Macro.WithArgs withArgs = macro.getWithArgs(); + return withArgs == null ? null : new WithArgsState(withArgs.getByName(), withArgs.getByPosition(), + withArgs.isOrderLast()); + } + + private static final class WithArgsState { + private final TemplateHashModelEx byName; + private final TemplateSequenceModel byPosition; + private final boolean orderLast; + private List<NameValuePair> orderLastByNameCatchAll; + + public WithArgsState(TemplateHashModelEx byName, TemplateSequenceModel byPosition, boolean orderLast) { + this.byName = byName; + this.byPosition = byPosition; + this.orderLast = orderLast; + } + } + + private static final class NameValuePair { + private final String name; + private final TemplateModel value; + + public NameValuePair(String name, TemplateModel value) { + this.name = name; + this.value = value; + } + } + private _MiscTemplateException newTooManyArgumentsException(Macro macro, String[] argNames, int argsCnt) { return new _MiscTemplateException(this, (macro.isFunction() ? "Function " : "Macro "), new _DelayedJQuote(macro.getName()), @@ -1057,7 +1138,7 @@ public final class Environment extends Configurable { return new _MiscTemplateException(this, (macro.isFunction() ? "Function " : "Macro "), new _DelayedJQuote(macro.getName()), " has no parameter with name ", new _DelayedJQuote(argName), ". Valid parameter names are: " - , new _DelayedJoinWithComma(macro.getArgumentNames())); + , new _DelayedJoinWithComma(macro.getArgumentNamesNoCopy())); } private _MiscTemplateException newBothNamedAndPositionalCatchAllParamsException(Macro macro) { diff --git a/src/main/java/freemarker/core/Macro.java b/src/main/java/freemarker/core/Macro.java index d1a81d0..4e564ef 100644 --- a/src/main/java/freemarker/core/Macro.java +++ b/src/main/java/freemarker/core/Macro.java @@ -115,16 +115,24 @@ public final class Macro extends TemplateElement implements TemplateModel { public String getCatchAll() { return catchAllParamName; } - + + /** + * Returns a new copy of the array that stored the names of arguments declared in this macro or function. + */ public String[] getArgumentNames() { return paramNames.clone(); } - String[] getArgumentNamesInternal() { + String[] getArgumentNamesNoCopy() { return paramNames; } - boolean hasArgNamed(String name) { + /** + * Returns if the macro or function has a parameter called as the argument. + * + * @since 2.3.30 + */ + public boolean hasArgNamed(String name) { return paramNamesWithDefault.containsKey(name); } @@ -480,15 +488,18 @@ public final class Macro extends TemplateElement implements TemplateModel { static final class WithArgs { private final TemplateHashModelEx byName; private final TemplateSequenceModel byPosition; + private final boolean orderLast; - WithArgs(TemplateHashModelEx byName) { + WithArgs(TemplateHashModelEx byName, boolean orderLast) { this.byName = byName; this.byPosition = null; + this.orderLast = orderLast; } - WithArgs(TemplateSequenceModel byPosition) { + WithArgs(TemplateSequenceModel byPosition, boolean orderLast) { this.byName = null; this.byPosition = byPosition; + this.orderLast = orderLast; } public TemplateHashModelEx getByName() { @@ -498,6 +509,10 @@ public final class Macro extends TemplateElement implements TemplateModel { public TemplateSequenceModel getByPosition() { return byPosition; } + + public boolean isOrderLast() { + return orderLast; + } } } diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml index dc47af5..d9717e1 100644 --- a/src/manual/en_US/book.xml +++ b/src/manual/en_US/book.xml @@ -13183,6 +13183,11 @@ grant codeBase "file:/path/to/freemarker.jar" <listitem> <para><link + linkend="ref_builtin_with_args_last">with_args_last</link></para> + </listitem> + + <listitem> + <para><link linkend="ref_builtin_word_list">word_list</link></para> </listitem> @@ -19877,6 +19882,152 @@ Same as: there:</para> <programlisting role="template"><@myMacro?with_args({'a': 1})>...</<emphasis>@myMacro</emphasis>></programlisting> + + <para>Note that as far as the order of arguments is concerned, + arguments coming from + <literal>with_args(<replaceable>...</replaceable>)</literal> are + added before the arguments specified in the call to the returned + directive/function/method. In some use cases it's more desirable to + add them at the end instead, in which case use the <link + linkend="ref_builtin_with_args_last"><literal>with_args_last</literal> + built-in</link>.</para> + </section> + + <section xml:id="ref_builtin_with_args_last"> + <title>with_args_last</title> + + <note> + <para>This built-in is available since 2.3.30</para> + </note> + + <para>Same as <link + linkend="ref_builtin_with_args"><literal>with_args</literal></link>, + but if the order of the arguments in resulting final argument list + may differs (but not the values in it). This only matters if you + pass parameters by position (typically, when calling functions or + methods), or when there's catch-all argument.</para> + + <para>A typical example with positional arguments is when you want + to add the dynamic argument to the end of the parameter list:</para> + + <programlisting role="template"><#function f a b c d> + <#return "a=${a}, b=${b}, c=${c}, d=${d}"> +</#function> + +<#assign dynamicArgs=[3, 4]> + +with_args: +${f?with_args(dynamicArgs)(1, 2)} + +with_args_last: +${f?with_args_last(dynamicArgs)(1, 2)}</programlisting> + + <programlisting role="output">with_args: +a=3, b=4, c=1, d=2 + +with_args_last: +a=1, b=2, c=3, d=4</programlisting> + + <para>In the case of name arguments, while the primary mean of + identifying an argument is the its name, catch-all arguments + (<literal>others...</literal> below) still have an order:</para> + + <programlisting role="template"><#macro m a b others...> + a=${a} + b=${b} + others: + <#list others as k, v> + ${k} = ${v} + </#list> +</#macro> + +<#assign dynamicArgs={'e': 5, 'f': 6}> + +with_args: +<@m?with_args(dynamicArgs) a=1 b=2 c=3 d=4 /> + +with_args_last: +<@m?with_args_last(dynamicArgs) a=1 b=2 c=3 d=4 /></programlisting> + + <programlisting role="output">with_args: + a=1 + b=2 + others: + e = 5 + f = 6 + c = 3 + d = 4 + +with_args_last: + a=1 + b=2 + others: + c = 3 + d = 4 + e = 5 + f = 6</programlisting> + + <para>If you specify a named parameter that are not catch-all, so + they are declared in the <literal>macro</literal> tag (as + <literal>a</literal> and <literal>b</literal> below), then + <literal>with_args</literal> and <literal>with_args_last</literal> + are no different, since the argument order is specified by the macro + definition, not the macro call:</para> + + <programlisting role="template"><#macro m a=0 b=0> + <#-- We use .args to demonstrate the ordering of a and b: --> + <#list .args as k, v> + ${k} = ${v} + </#list> +</#macro> + +<#assign dynamicArgs={'b': 1}> + +with_args: +<@m?with_args(dynamicArgs) a=1 /> + +with_args_last: +<@m?with_args_last(dynamicArgs) a=1 /></programlisting> + + <programlisting role="output">with_args: + a = 1 + b = 1 + +with_args_last: + a = 1 + b = 1</programlisting> + + <para>If both the macro or directive call, and the + <literal>with_args_last</literal> argument specifies named catch-all + argument with the same name (like <literal>b</literal> below), then + the placement of those parameters is decide by the macro/directive + call:</para> + + <programlisting role="template"><#macro m others...> + <#list others as k, v> + ${k} = ${v} + </#list> +</#macro> + +<#assign dynamicArgs={'b': 0, 'd': 4}> + +with_args: +<@m?with_args(dynamicArgs) a=1 b=2 c=3 /> + +with_args_last: +<@m?with_args_last(dynamicArgs) a=1 b=2 c=3 /></programlisting> + + <programlisting role="output">with_args: +<emphasis> b = 2 + d = 4 +</emphasis> a = 1 + c = 3 + +with_args_last: + a = 1 +<emphasis> b = 2 +</emphasis> c = 3 +<emphasis> d = 4</emphasis></programlisting> </section> </section> </chapter> @@ -28988,8 +29139,10 @@ TemplateModel x = env.getVariable("x"); // get variable x</programlisting> xlink:href="https://issues.apache.org/jira/browse/FREEMARKER-107">FREEMARKER-107</link>: Added <literal>?<replaceable>with_args</replaceable>(dynamicArguments)</literal> + and + <literal>?<replaceable>with_args_last</replaceable>(dynamicArguments)</literal> to add parameters dynamically to directive (like macro), - function and method calls. Actually, this built-in returns + function and method calls. Actually, this built-in returns a directive or macro or function that has different parameter defaults. <link linkend="ref_builtin_with_args">See more here...</link></para> diff --git a/src/test/java/freemarker/core/WithArgsBuiltInTest.java b/src/test/java/freemarker/core/WithArgsBuiltInTest.java index 3712509..71366e6 100644 --- a/src/test/java/freemarker/core/WithArgsBuiltInTest.java +++ b/src/test/java/freemarker/core/WithArgsBuiltInTest.java @@ -20,6 +20,7 @@ package freemarker.core; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -42,7 +43,7 @@ import freemarker.test.TemplateTest; public class WithArgsBuiltInTest extends TemplateTest { - private static final String PRINT_O = "o=<#if o?isSequence>[${o?join(', ')}]" + + private static final String PRINT_O = "o=<#if o?isSequence>[<#list o as v>${v!'null'}<#sep>, </#list>]" + "<#else>{<#list o as k,v>${k}=${v!'null'}<#sep>, </#list>}" + "</#if>"; @@ -179,10 +180,10 @@ public class WithArgsBuiltInTest extends TemplateTest { @Test public void testNullsWithMacroWithPositionalWithArgs() throws Exception { // Null-s in ?withArgs 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 + assertOutput("<@mCAO 1 null null 4 />", "o=[1, null, null, 4]"); addToDataModel("args", Arrays.asList(1, null, null, 4)); - assertOutput("<@mCAO?withArgs(args) />", "o=[1, 4]"); // [FM3] See above - assertOutput("<@mCAO?withArgs(args) null 5 6 />", "o=[1, 4, 5, 6]"); // [FM3] See above + assertOutput("<@mCAO?withArgs(args) />", "o=[1, null, null, 4]"); + assertOutput("<@mCAO?withArgs(args) null 5 6 />", "o=[1, null, null, 4, null, 5, 6]"); } @Test @@ -216,10 +217,10 @@ public class WithArgsBuiltInTest extends TemplateTest { @Test public void testNullsWithFunction() throws Exception { // Null-s in ?withArgs 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 + assertOutput("${fCAO(1, null, null, 4)}", "o=[1, null, null, 4]"); addToDataModel("args", Arrays.asList(1, null, null, 4)); - assertOutput("${fCAO?withArgs(args)()}", "o=[1, 4]"); // [FM3] See above - assertOutput("${fCAO?withArgs(args)(null, 5, 6)}", "o=[1, 4, 5, 6]"); // [FM3] See above + assertOutput("${fCAO?withArgs(args)()}", "o=[1, null, null, 4]"); + assertOutput("${fCAO?withArgs(args)(null, 5, 6)}", "o=[1, null, null, 4, null, 5, 6]"); } @Test @@ -322,6 +323,18 @@ public class WithArgsBuiltInTest extends TemplateTest { assertOutput("${obj.mNullable?withArgs(args)()}", "null, 2, null"); } + @Test + public void testMethodWithArgsLast() throws IOException, TemplateException { + addToDataModel("obj", new MethodHolder()); + assertOutput("${obj.m3p?withArgsLast([1, 2, 3])()}", "1, 2, 3"); + assertOutput("${obj.m3p?withArgsLast([1, 2])(3)}", "3, 1, 2"); + assertOutput("${obj.m3p?withArgsLast([1])(2, 3)}", "2, 3, 1"); + assertOutput("${obj.m3p?withArgsLast([])(1, 2, 3)}", "1, 2, 3"); + + addToDataModel("args", Arrays.asList(null, 2)); + assertOutput("${obj.mNullable?withArgsLast(args)(1)}", "1, null, 2"); + } + public static class MethodHolder { public String m3p(int a, int b, int c) { return a + ", " + b + ", " + c; @@ -400,6 +413,126 @@ public class WithArgsBuiltInTest extends TemplateTest { "{a=null, b=22, c=null, e=6, d=55}{}"); } + @Test + public void testTemplateDirectiveModelWithArgsLast() throws IOException, TemplateException { + addToDataModel("directive", new TestTemplateDirectiveModel()); + + Map<String, Integer> args = new LinkedHashMap<String, Integer>(); + args.put("a", null); + args.put("b", 2); + args.put("c", 3); + args.put("e", 6); + args.put("f", 7); + args.put("g", null); + addToDataModel("args", args); + + assertOutput("<@directive?withArgsLast(args) b=22 c=null d=55 />", + "{b=22, c=null, d=55, a=null, e=6, f=7, g=null}{}"); + + assertOutput("<@directive?withArgsLast({}) b=22 c=null d=55 />", + "{b=22, c=null, d=55}{}"); + + assertOutput("<@directive?withArgsLast(args) />", + "{a=null, b=2, c=3, e=6, f=7, g=null}{}"); + } + + @Test + public void testMacroWithArgsLastNamed() throws IOException, TemplateException { + assertOutput("<@m?withArgsLast({'a': 1, 'b': 2}) />", "a=1; b=2; c=d3"); + assertOutput("<@m?withArgsLast({'b': 2}) a=1 />", "a=1; b=2; c=d3"); + assertOutput("<@m?withArgsLast({}) a=1 b=2 />", "a=1; b=2; c=d3"); + + assertOutput("<@m?withArgsLast({'a': 1, 'b': 2, 'c': 3}) />", "a=1; b=2; c=3"); + assertOutput("<@m?withArgsLast({'b': 2}) a=1 c=3 />", "a=1; b=2; c=3"); + assertOutput("<@m?withArgsLast({'c': 3}) a=1 b=2 />", "a=1; b=2; c=3"); + assertOutput("<@m?withArgsLast({}) a=1 b=2 c=3 />", "a=1; b=2; c=3"); + + assertOutput("<@m?withArgsLast({'b': 2}) 1 />", "a=1; b=2; c=d3"); + assertOutput("<@m?withArgsLast({'c': 3}) 1 2 />", "a=1; b=2; c=3"); + assertOutput("<@m?withArgsLast({'b': 22, 'c': 3}) 1 2 />", "a=1; b=2; c=3"); + + assertOutput("<@mCA?withArgsLast({'a': 1, 'b': 2, 'c': 3, 'd': 4}) />", "a=1; b=2; o={c=3, d=4}"); + assertOutput("<@mCA?withArgsLast({'b': 2, 'c': 3, 'd': 4}) a=1 />", "a=1; b=2; o={c=3, d=4}"); + assertOutput("<@mCA?withArgsLast({'c': 3, 'd': 4}) a=1 b=2 />", "a=1; b=2; o={c=3, d=4}"); + assertOutput("<@mCA?withArgsLast({'d': 4}) a=1 b=2 c=3 />", "a=1; b=2; o={c=3, d=4}"); + assertOutput("<@mCA?withArgsLast({}) a=1 b=2 c=3 d=4 />", "a=1; b=2; o={c=3, d=4}"); + + assertOutput("<@mCA?withArgsLast({'a': 11}) 1 2 />", "a=1; b=2; o=[]"); + assertOutput("<@mCA?withArgsLast({'a': 11, 'c': 3}) 1 2 />", "a=1; b=2; o={c=3}"); + assertErrorContains("<@mCA?withArgsLast({'a': 11, 'c': 3}) 1 2 3 />", "both named and positional", "catch-all"); + assertOutput("<@mCA?withArgsLast({'a': 11, 'b': 22}) 1 2 3 />", "a=1; b=2; o=[3]"); + + assertOutput("<@mCAO?withArgsLast({'a': 1, 'b': 2}) />", "o={a=1, b=2}"); + assertOutput("<@mCAO?withArgsLast({'b': 2}) a=1 />", "o={a=1, b=2}"); + assertOutput("<@mCAO?withArgsLast({}) a=1 b=2 />", "o={a=1, b=2}"); + + assertOutput("<@mCAO?withArgsLast({}) />", "o={}"); + + // Ordering of "real" args win: + assertOutput("<@mCA?withArgsLast({'c': 3, 'd': 4}) a=1 b=2 />", "a=1; b=2; o={c=3, d=4}"); + assertOutput("<@mCA?withArgsLast({'c': 3, 'd': 4}) a=1 d=44 b=2 />", "a=1; b=2; o={d=44, c=3}"); + } + + @Test + public void testMacroWithArgsLastNamedNullArgs() throws IOException, TemplateException { + assertOutput("<@mCA?withArgsLast({'c': 3, 'd': 4}) a=1 d=null b=2 />", "a=1; b=2; o={d=null, c=3}"); + Map<String, Integer> cAndDNull = new LinkedHashMap<String, Integer>(); + cAndDNull.put("c", 3); + cAndDNull.put("d", null); + addToDataModel("cAndDNull", cAndDNull); + assertOutput("<@mCA?withArgsLast(cAndDNull) a=1 b=2 />", "a=1; b=2; o={c=3, d=null}"); + assertOutput("<@mCA?withArgsLast(cAndDNull) a=1 d=null b=2 />", "a=1; b=2; o={d=null, c=3}"); + } + + @Test + public void testMacroWithArgsLastPositional() throws IOException, TemplateException { + assertOutput("<@m?withArgsLast([1, 2, 3]) />", "a=1; b=2; c=3"); + assertOutput("<@m?withArgsLast([2, 3]) 1 />", "a=1; b=2; c=3"); + assertOutput("<@m?withArgsLast([3]) 1 2 />", "a=1; b=2; c=3"); + assertOutput("<@m?withArgsLast([]) 1 2 3 />", "a=1; b=2; c=3"); + + assertOutput("<@m?withArgsLast([]) a=1 b=2 />", "a=1; b=2; c=d3"); + assertErrorContains("<@m?withArgsLast([3]) a=1 b=2 />", "by name", "by position", "last"); + + assertOutput("<@m?withArgsLast([1, 2]) />", "a=1; b=2; c=d3"); + assertOutput("<@m?withArgsLast([2]) 1 />", "a=1; b=2; c=d3"); + assertOutput("<@m?withArgsLast([]) 1 2 />", "a=1; b=2; c=d3"); + + assertOutput("<@mCA?withArgsLast([1, 2, 3, 4]) />", "a=1; b=2; o=[3, 4]"); + assertOutput("<@mCA?withArgsLast([2, 3, 4]) 1 />", "a=1; b=2; o=[3, 4]"); + assertOutput("<@mCA?withArgsLast([3, 4]) 1 2 />", "a=1; b=2; o=[3, 4]"); + assertOutput("<@mCA?withArgsLast([4]) 1 2 3 />", "a=1; b=2; o=[3, 4]"); + assertOutput("<@mCA?withArgsLast([]) 1 2 3 4 />", "a=1; b=2; o=[3, 4]"); + + assertOutput("<@mCAO?withArgsLast([1, 2, 3, 4]) />", "o=[1, 2, 3, 4]"); + assertOutput("<@mCAO?withArgsLast([3, 4]) 1 2 />", "o=[1, 2, 3, 4]"); + assertOutput("<@mCAO?withArgsLast([]) 1 2 3 4 />", "o=[1, 2, 3, 4]"); + + assertOutput("<@mCAO?withArgsLast([]) a=1 b=2 />", "o={a=1, b=2}"); + assertErrorContains("<@mCAO?withArgsLast([3]) a=1 b=2 />", "by name", "by position", "last"); + + assertOutput("<@mCAO?withArgsLast([]) />", "o=[]"); + + assertErrorContains("<@m?withArgsLast([0, 0, 0, 0]) />", "3", "4", "parameter"); + assertErrorContains("<@m?withArgsLast([0, 0, 0]) 0 />", "3", "4", "parameter"); + assertErrorContains("<@m?withArgsLast([]) 0 0 0 0 />", "3", "4", "parameter"); + } + + @Test + public void testMacroWithArgsLastPositionalNullArgs() throws IOException, TemplateException { + ArrayList<Object> twoAndNull = new ArrayList<Object>(); + twoAndNull.add(2); + twoAndNull.add(null); + addToDataModel("twoAndNull", twoAndNull); + + assertOutput("<@m?withArgsLast(twoAndNull) 1 />", "a=1; b=2; c=d3"); + assertErrorContains("<@m?withArgsLast([3]) null 2 />", "\"a\"", "null"); + assertOutput("<@m?withArgsLast([]) 1 2 null />", "a=1; b=2; c=d3"); + + assertOutput("<@mCAO?withArgsLast(twoAndNull) 1 />", "o=[1, 2, null]"); + assertOutput("<@mCAO?withArgsLast([3]) null 2 />", "o=[null, 2, 3]"); + } + private static class TestTemplateDirectiveModel implements TemplateDirectiveModel { public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body) throws diff --git a/src/test/java/freemarker/manual/WithArgsLastExamples.java b/src/test/java/freemarker/manual/WithArgsLastExamples.java new file mode 100644 index 0000000..b59dca8 --- /dev/null +++ b/src/test/java/freemarker/manual/WithArgsLastExamples.java @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package freemarker.manual; + +import java.io.IOException; + +import org.junit.Test; + +import freemarker.template.TemplateException; + +public class WithArgsLastExamples extends ExamplesTest { + + @Test + public void usingWithArgsSpecialVariable() throws IOException, TemplateException { + assertOutputForNamed("WithArgsLastExamples.ftl"); + } + +} diff --git a/src/test/resources/freemarker/manual/WithArgsLastExamples.ftl b/src/test/resources/freemarker/manual/WithArgsLastExamples.ftl new file mode 100644 index 0000000..4494b13 --- /dev/null +++ b/src/test/resources/freemarker/manual/WithArgsLastExamples.ftl @@ -0,0 +1,25 @@ +<#function f a b c d> + <#return "a=${a}, b=${b}, c=${c}, d=${d}"> +</#function> + +${f?with_args([2, 3])(1, 2)} +${f?with_args_last([2, 3])(1, 2)} + +<#macro m a b others...> + a=${a} + b=${b} + others: + <#list others as k, v> + ${k} = ${v} + </#list> +</#macro> +<@m?with_args({'e': 5, 'f': 6}) a=1 b=2 c=3 d=4 /> +<@m?with_args_last({'e': 5, 'f': 6}) a=1 b=2 c=3 d=4 /> + +<#macro m a b others...> + <#list .args as k, v> + ${k} = ${v} + </#list> +</#macro> +<@m?with_args({'e': 5, 'f': 6}) a=1 b=2 c=3 d=4 /> +<@m?with_args_last({'e': 5, 'f': 6}) a=1 b=2 c=3 d=4 />