You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2017/08/07 22:32:04 UTC
[02/21] incubator-freemarker git commit: FREEMARKER-63: Very early
state. Until it's fully functional,
the new interface is called TemplateDirectiveModel2,
and is invoked with <~...> instead of <@...>. Later it will replace
TemplateDirectiveModel and th
FREEMARKER-63: Very early state. Until it's fully functional, the new interface is called TemplateDirectiveModel2, and is invoked with <~...> instead of <@...>. Later it will replace TemplateDirectiveModel and the syntax will be <@...>.
Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/c28a78bd
Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/c28a78bd
Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/c28a78bd
Branch: refs/heads/3
Commit: c28a78bd8dad4bfd3a37a90a26fbb15639d33604
Parents: 4295e24
Author: ddekany <dd...@apache.org>
Authored: Mon Jul 24 20:26:29 2017 +0200
Committer: ddekany <dd...@apache.org>
Committed: Tue Jul 25 00:39:22 2017 +0200
----------------------------------------------------------------------
.../core/TemplateCallableModelTest.java | 130 ++++++
.../freemarker/core/model/ConstantsTest.java | 21 +
.../core/userpkg/AllFeaturesDirective.java | 133 ++++++
.../userpkg/TestTemplateDirectiveModel.java | 88 ++++
.../core/userpkg/TwoNamedParamsDirective.java | 85 ++++
.../userpkg/TwoPositionalParamsDirective.java | 71 ++++
.../core/ASTDirDynamicDirectiveCall.java | 422 +++++++++++++++++++
.../freemarker/core/NonNumericalException.java | 12 +-
.../NonUserDefinedDirectiveLikeException.java | 1 +
.../core/TemplateCallableModelUtils.java | 48 +++
.../core/UnexpectedTypeException.java | 69 ++-
.../freemarker/core/_DelayedJQuotedListing.java | 46 ++
.../apache/freemarker/core/model/CallPlace.java | 173 ++++++++
.../apache/freemarker/core/model/Constants.java | 36 +-
.../core/model/TemplateCallableModel.java | 86 ++++
.../core/model/TemplateDirectiveModel2.java | 28 ++
.../core/model/TemplateFunctionModel.java | 25 ++
.../apache/freemarker/core/util/FTLUtil.java | 1 +
freemarker-core/src/main/javacc/FTL.jj | 309 ++++++++++++--
19 files changed, 1729 insertions(+), 55 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateCallableModelTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateCallableModelTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateCallableModelTest.java
new file mode 100644
index 0000000..67b73d0
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateCallableModelTest.java
@@ -0,0 +1,130 @@
+/*
+ * 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 org.apache.freemarker.core;
+
+import java.io.IOException;
+
+import org.apache.freemarker.core.userpkg.AllFeaturesDirective;
+import org.apache.freemarker.core.userpkg.TwoNamedParamsDirective;
+import org.apache.freemarker.core.userpkg.TwoPositionalParamsDirective;
+import org.apache.freemarker.test.TemplateTest;
+import org.junit.Before;
+import org.junit.Test;
+
+public class TemplateCallableModelTest extends TemplateTest {
+
+ @Before
+ public void addCommonData() {
+ addToDataModel("a", new AllFeaturesDirective());
+ addToDataModel("p", new TwoPositionalParamsDirective());
+ addToDataModel("n", new TwoNamedParamsDirective());
+ }
+
+ @Test
+ public void testBasicCall() throws IOException, TemplateException {
+ assertOutput("<~p />",
+ "#p(p1=null, p2=null)");
+ assertOutput("<~p 1 />",
+ "#p(p1=1, p2=null)");
+ assertOutput("<~p 1, 2 />",
+ "#p(p1=1, p2=2)");
+
+ assertOutput("<~n />",
+ "#n(n1=null, n2=null)");
+ assertOutput("<~n n1=11/>",
+ "#n(n1=11, n2=null)");
+ assertOutput("<~n n1=11 n2=22/>",
+ "#n(n1=11, n2=22)");
+
+ assertOutput("<~a />",
+ "#a(p1=null, p2=null, pOthers=[], n1=null, n2=null, nOthers={})");
+ assertOutput("<~a 1, 2 />",
+ "#a(p1=1, p2=2, pOthers=[], n1=null, n2=null, nOthers={})");
+ assertOutput("<~a n1=11 n2=22 />",
+ "#a(p1=null, p2=null, pOthers=[], n1=11, n2=22, nOthers={})");
+
+ assertOutput("<~a 1, 2 n1=11 n2=22 />",
+ "#a(p1=1, p2=2, pOthers=[], n1=11, n2=22, nOthers={})");
+ assertOutput("<~a 1 n1=11 />",
+ "#a(p1=1, p2=null, pOthers=[], n1=11, n2=null, nOthers={})");
+ assertOutput("<~a 1, 2, 3 n1=11 n2=22 n3=33 />",
+ "#a(p1=1, p2=2, pOthers=[3], n1=11, n2=22, nOthers={\"n3\": 33})");
+ assertOutput("<~a 1 n1=11 n3=33 />",
+ "#a(p1=1, p2=null, pOthers=[], n1=11, n2=null, nOthers={\"n3\": 33})");
+ assertOutput("<~a 1 n1=11 a=1 b=2 c=3 d=4 e=5 f=6 g=7 />",
+ "#a(p1=1, p2=null, pOthers=[], n1=11, n2=null, nOthers={"
+ + "\"a\": 1, \"b\": 2, \"c\": 3, \"d\": 4, \"e\": 5, \"f\": 6, \"g\": 7})");
+
+ assertOutput("<~a; a, b, c/>",
+ "#a(p1=null, p2=null, pOthers=[], n1=null, n2=null, nOthers={}; 3)");
+ assertOutput("<~a 1, 2; a, b, c />",
+ "#a(p1=1, p2=2, pOthers=[], n1=null, n2=null, nOthers={}; 3)");
+ assertOutput("<~a n1=11 n2=22; a, b, c />",
+ "#a(p1=null, p2=null, pOthers=[], n1=11, n2=22, nOthers={}; 3)");
+ assertOutput("<~a 1, 2 n1=11 n2=22; a, b, c />",
+ "#a(p1=1, p2=2, pOthers=[], n1=11, n2=22, nOthers={}; 3)");
+
+ assertOutput("<~a></~a>",
+ "#a(p1=null, p2=null, pOthers=[], n1=null, n2=null, nOthers={})");
+ assertOutput("<~a>x</~a>",
+ "#a(p1=null, p2=null, pOthers=[], n1=null, n2=null, nOthers={}) {...}");
+ }
+
+ @Test
+ public void testSyntaxEdgeCases() throws IOException, TemplateException {
+ assertOutput("<~a; x/>",
+ "#a(p1=null, p2=null, pOthers=[], n1=null, n2=null, nOthers={}; 1)");
+ assertOutput("<~a;x/>",
+ "#a(p1=null, p2=null, pOthers=[], n1=null, n2=null, nOthers={}; 1)");
+ assertOutput("<~a;x />",
+ "#a(p1=null, p2=null, pOthers=[], n1=null, n2=null, nOthers={}; 1)");
+
+ assertOutput("<~a 1 , 2 n1 = 11 n2 = 22 ; a , b , c />",
+ "#a(p1=1, p2=2, pOthers=[], n1=11, n2=22, nOthers={}; 3)");
+ assertOutput("<~a 1<#-- -->,<#-- -->2<#-- -->n1<#-- -->=<#-- -->11<#-- -->n2=22<#-- -->;"
+ + "<#-- -->a<#-- -->,<#-- -->b<#-- -->,<#-- -->c<#-- -->/>",
+ "#a(p1=1, p2=2, pOthers=[], n1=11, n2=22, nOthers={}; 3)");
+ assertOutput("<~a\t1,2\tn1=11\tn2=22;a,b,c/>",
+ "#a(p1=1, p2=2, pOthers=[], n1=11, n2=22, nOthers={}; 3)");
+
+ assertOutput("<~a + 1 />",
+ "#a(p1=1, p2=null, pOthers=[], n1=null, n2=null, nOthers={})");
+ }
+
+ @Test
+ public void testParsingErrors() throws IOException, TemplateException {
+ assertErrorContains("<~a, n1=1 />", "Remove comma", "between", "by position");
+ assertErrorContains("<~a n1=1, n2=1 />", "Remove comma", "between", "by position");
+ assertErrorContains("<~a n1=1, 2 />", "Remove comma", "between", "by position");
+ assertErrorContains("<~a, 1 />", "Remove comma", "between", "by position");
+ assertErrorContains("<~a 1, , 2 />", "Two commas");
+ assertErrorContains("<~a 1 2 />", "Missing comma");
+ assertErrorContains("<~a n1=1 2 />", "must be earlier than arguments passed by name");
+ }
+
+ @Test
+ public void testRuntimeErrors() throws IOException, TemplateException {
+ assertErrorContains("<~p 9, 9, 9 />", "can only have 2", "3", "by position");
+ assertErrorContains("<~n 9 />", "can't have arguments passed by position");
+ assertErrorContains("<~n n3=9 />", "has no", "\"n3\"", "supported", "\"n1\", \"n2\"");
+ assertErrorContains("<~p n1=9 />", "doesn't have any by-name-passed");
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/org/apache/freemarker/core/model/ConstantsTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/org/apache/freemarker/core/model/ConstantsTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/org/apache/freemarker/core/model/ConstantsTest.java
new file mode 100644
index 0000000..0817bf9
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/org/apache/freemarker/core/model/ConstantsTest.java
@@ -0,0 +1,21 @@
+package org.apache.freemarker.core.model.impl.org.apache.freemarker.core.model;
+
+import java.io.IOException;
+
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.core.model.Constants;
+import org.apache.freemarker.test.TemplateTest;
+import org.junit.Test;
+
+public final class ConstantsTest extends TemplateTest {
+
+ @Test
+ public void testEmptyHash() throws IOException, TemplateException {
+ addToDataModel("h", Constants.EMPTY_HASH);
+ assertOutput("{<#list h as k ,v>x</#list>}", "{}");
+ assertOutput("{<#list h?keys as k>x</#list>}", "{}");
+ assertOutput("{<#list h?values as k>x</#list>}", "{}");
+ assertOutput("${h?size}", "0");
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/AllFeaturesDirective.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/AllFeaturesDirective.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/AllFeaturesDirective.java
new file mode 100644
index 0000000..b7baf56
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/AllFeaturesDirective.java
@@ -0,0 +1,133 @@
+/*
+ * 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 org.apache.freemarker.core.userpkg;
+
+import static org.apache.freemarker.core.TemplateCallableModelUtils.*;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Collection;
+import java.util.Map;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.core.model.CallPlace;
+import org.apache.freemarker.core.model.TemplateHashModelEx2;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+
+import com.google.common.collect.ImmutableMap;
+
+public class AllFeaturesDirective extends TestTemplateDirectiveModel {
+
+ private static final int P1_ARG_IDX = 0;
+ private static final int P2_ARG_IDX = 1;
+ private static final int P_OTHERS_ARG_IDX = 2;
+ private static final int N1_ARG_IDX = 3;
+ private static final int N2_ARG_IDX = 4;
+ private static final int N_OTHERS_IDX = 5;
+
+ private static final String N1_ARG_NAME = "n1";
+ private static final String N2_ARG_NAME = "n2";
+
+ private final boolean p1AllowNull;
+ private final boolean p2AllowNull;
+ private final boolean n1AllowNull;
+ private final boolean n2AllowNull;
+
+ public AllFeaturesDirective() {
+ this(true, true, true, true);
+ }
+
+ public AllFeaturesDirective(boolean p1AllowNull, boolean p2AllowNull, boolean n1AllowNull, boolean n2AllowNull) {
+ this.p1AllowNull = p1AllowNull;
+ this.p2AllowNull = p2AllowNull;
+ this.n1AllowNull = n1AllowNull;
+ this.n2AllowNull = n2AllowNull;
+ }
+
+ private static final Map<String, Integer> PARAM_NAME_TO_IDX = new ImmutableMap.Builder<String, Integer>()
+ .put(N1_ARG_NAME, N1_ARG_IDX)
+ .put(N2_ARG_NAME, N2_ARG_IDX)
+ .build();
+
+ @Override
+ public void execute(TemplateModel[] args, Writer out, Environment env, CallPlace callPlace)
+ throws TemplateException, IOException {
+ execute(castArgumentToNumber(args, P1_ARG_IDX, p1AllowNull, env),
+ castArgumentToNumber(args, P2_ARG_IDX, p2AllowNull, env),
+ (TemplateSequenceModel) args[P_OTHERS_ARG_IDX],
+ castArgumentToNumber(args[N1_ARG_IDX], N1_ARG_NAME, n1AllowNull, env),
+ castArgumentToNumber(args[N2_ARG_IDX], N2_ARG_NAME, n2AllowNull, env),
+ (TemplateHashModelEx2) args[N_OTHERS_IDX],
+ out, env, callPlace);
+ }
+
+ private void execute(TemplateNumberModel p1, TemplateNumberModel p2, TemplateSequenceModel pOthers,
+ TemplateNumberModel n1, TemplateNumberModel n2, TemplateHashModelEx2 nOthers,
+ Writer out, Environment env, CallPlace callPlace) throws IOException, TemplateException {
+ out.write("#a(");
+ printParam("p1", p1, out, true);
+ printParam("p2", p2, out);
+ printParam("pOthers", pOthers, out);
+ printParam(N1_ARG_NAME, n1, out);
+ printParam(N2_ARG_NAME, n2, out);
+ printParam("nOthers", nOthers, out);
+ if (callPlace.getLoopVariableCount() != 0) {
+ out.write("; " + callPlace.getLoopVariableCount());
+ }
+ out.write(")");
+ if (callPlace.hasNestedContent()) {
+ out.write(" {...}");
+ }
+ }
+
+ @Override
+ public int getPredefinedPositionalArgumentCount() {
+ return 2;
+ }
+
+ @Override
+ public boolean hasPositionalVarargsArgument() {
+ return true;
+ }
+
+ @Override
+ public int getNamedArgumentIndex(String name) {
+ Integer idx = PARAM_NAME_TO_IDX.get(name);
+ return idx != null ? idx : -1;
+ }
+
+ @Override
+ public int getNamedVarargsArgumentIndex() {
+ return N_OTHERS_IDX;
+ }
+
+ @Override
+ public Collection<String> getPredefinedNamedArgumentNames() {
+ return PARAM_NAME_TO_IDX.keySet();
+ }
+
+ @Override
+ public int getTotalArgumentCount() {
+ return N_OTHERS_IDX + 1;
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TestTemplateDirectiveModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TestTemplateDirectiveModel.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TestTemplateDirectiveModel.java
new file mode 100644
index 0000000..c69497a
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TestTemplateDirectiveModel.java
@@ -0,0 +1,88 @@
+/*
+ * 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 org.apache.freemarker.core.userpkg;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import org.apache.freemarker.core.model.TemplateDirectiveModel2;
+import org.apache.freemarker.core.model.TemplateHashModelEx2;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.util.FTLUtil;
+
+public abstract class TestTemplateDirectiveModel implements TemplateDirectiveModel2 {
+
+ protected void printParam(String name, TemplateModel value, Writer out) throws IOException, TemplateModelException {
+ printParam(name, value, out, false);
+ }
+
+ protected void printParam(String name, TemplateModel value, Writer out, boolean first)
+ throws IOException, TemplateModelException {
+ if (!first) {
+ out.write(", ");
+ }
+ out.write(name);
+ out.write("=");
+ printValue(value, out);
+ }
+
+ private void printValue(TemplateModel value, Writer out) throws IOException, TemplateModelException {
+ if (value == null) {
+ out.write("null");
+ } else if (value instanceof TemplateNumberModel) {
+ out.write(((TemplateNumberModel) value).getAsNumber().toString());
+ } else if (value instanceof TemplateScalarModel) {
+ out.write(FTLUtil.toStringLiteral(((TemplateScalarModel) value).getAsString()));
+ } else if (value instanceof TemplateSequenceModel) {
+ int len = ((TemplateSequenceModel) value).size();
+ out.write('[');
+ for (int i = 0; i < len; i++) {
+ if (i != 0) {
+ out.write(", ");
+ }
+ printValue(((TemplateSequenceModel) value).get(i), out);
+ }
+ out.write(']');
+ } else if (value instanceof TemplateHashModelEx2) {
+ TemplateHashModelEx2.KeyValuePairIterator it = ((TemplateHashModelEx2) value).keyValuePairIterator();
+ out.write('{');
+ while (it.hasNext()) {
+ TemplateHashModelEx2.KeyValuePair kvp = it.next();
+
+ printValue(kvp.getKey(), out);
+ out.write(": ");
+ printValue(kvp.getValue(), out);
+
+ if (it.hasNext()) {
+ out.write(", ");
+ }
+ }
+ out.write('}');
+ } else {
+ throw new IllegalArgumentException("Unsupported value class: " + value.getClass().getName());
+ }
+ }
+
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TwoNamedParamsDirective.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TwoNamedParamsDirective.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TwoNamedParamsDirective.java
new file mode 100644
index 0000000..4d2d635
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TwoNamedParamsDirective.java
@@ -0,0 +1,85 @@
+/*
+ * 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 org.apache.freemarker.core.userpkg;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Collection;
+import java.util.Map;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.core.model.CallPlace;
+import org.apache.freemarker.core.model.TemplateModel;
+
+import com.google.common.collect.ImmutableMap;
+
+public class TwoNamedParamsDirective extends TestTemplateDirectiveModel {
+
+ private static final String N1_ARG_NAME = "n1";
+ private static final String N2_ARG_NAME = "n2";
+ private static final int N1_ARG_IDX = 0;
+ private static final int N2_ARG_IDX = 1;
+
+ private static final Map<String, Integer> PARAM_NAME_TO_IDX = new ImmutableMap.Builder<String, Integer>()
+ .put(N1_ARG_NAME, N1_ARG_IDX)
+ .put(N2_ARG_NAME, N2_ARG_IDX)
+ .build();
+
+ @Override
+ public void execute(TemplateModel[] args, Writer out, Environment env, CallPlace callPlace)
+ throws TemplateException, IOException {
+ out.write("#n(");
+ printParam(N1_ARG_NAME, args[N1_ARG_IDX], out, true);
+ printParam(N2_ARG_NAME, args[N2_ARG_IDX], out);
+ out.write(")");
+ }
+
+ @Override
+ public int getPredefinedPositionalArgumentCount() {
+ return 0;
+ }
+
+ @Override
+ public boolean hasPositionalVarargsArgument() {
+ return false;
+ }
+
+ @Override
+ public int getNamedArgumentIndex(String name) {
+ Integer idx = PARAM_NAME_TO_IDX.get(name);
+ return idx != null ? idx : -1;
+ }
+
+ @Override
+ public int getNamedVarargsArgumentIndex() {
+ return -1;
+ }
+
+ @Override
+ public int getTotalArgumentCount() {
+ return PARAM_NAME_TO_IDX.size();
+ }
+
+ @Override
+ public Collection<String> getPredefinedNamedArgumentNames() {
+ return PARAM_NAME_TO_IDX.keySet();
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TwoPositionalParamsDirective.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TwoPositionalParamsDirective.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TwoPositionalParamsDirective.java
new file mode 100644
index 0000000..e9eb926
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/userpkg/TwoPositionalParamsDirective.java
@@ -0,0 +1,71 @@
+/*
+ * 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 org.apache.freemarker.core.userpkg;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.util.Collection;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.core.model.CallPlace;
+import org.apache.freemarker.core.model.TemplateModel;
+
+public class TwoPositionalParamsDirective extends TestTemplateDirectiveModel {
+
+ @Override
+ public void execute(TemplateModel[] args, Writer out, Environment env, CallPlace callPlace)
+ throws TemplateException, IOException {
+ out.write("#p(");
+ printParam("p1", args[0], out, true);
+ printParam("p2", args[1], out);
+ out.write(")");
+ }
+
+ @Override
+ public int getPredefinedPositionalArgumentCount() {
+ return 2;
+ }
+
+ @Override
+ public boolean hasPositionalVarargsArgument() {
+ return false;
+ }
+
+ @Override
+ public int getNamedArgumentIndex(String name) {
+ return -1;
+ }
+
+ @Override
+ public int getNamedVarargsArgumentIndex() {
+ return -1;
+ }
+
+ @Override
+ public int getTotalArgumentCount() {
+ return getPredefinedPositionalArgumentCount();
+ }
+
+ @Override
+ public Collection<String> getPredefinedNamedArgumentNames() {
+ return null;
+ }
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirDynamicDirectiveCall.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirDynamicDirectiveCall.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirDynamicDirectiveCall.java
new file mode 100644
index 0000000..52ae027
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTDirDynamicDirectiveCall.java
@@ -0,0 +1,422 @@
+/*
+ * 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 org.apache.freemarker.core;
+
+import java.io.IOException;
+import java.util.Collection;
+
+import org.apache.freemarker.core.model.CallPlace;
+import org.apache.freemarker.core.model.Constants;
+import org.apache.freemarker.core.model.TemplateCallableModel;
+import org.apache.freemarker.core.model.TemplateDirectiveModel2;
+import org.apache.freemarker.core.model.TemplateFunctionModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateSequenceModel;
+import org.apache.freemarker.core.util.BugException;
+import org.apache.freemarker.core.util.CommonSupplier;
+import org.apache.freemarker.core.util._StringUtil;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+class ASTDirDynamicDirectiveCall extends ASTDirective implements CallPlace {
+
+ static final class NamedArgument {
+ private final String name;
+ private final ASTExpression value;
+
+ public NamedArgument(String name, ASTExpression value) {
+ this.name = name;
+ this.value = value;
+ }
+ }
+
+ private final ASTExpression callableValueExp;
+ private final ASTExpression[] positionalArgs;
+ private final NamedArgument[] namedArgs;
+ private final String[] loopVarNames;
+ private final boolean allowCallingFunctions;
+
+ private CustomDataHolder customDataHolder;
+
+ /**
+ * @param allowCallingFunctions Some template languages may allow calling {@link TemplateFunctionModel}-s
+ * directly embedded into the static text, in which case this should be {@code true}.
+ */
+ ASTDirDynamicDirectiveCall(
+ ASTExpression callableValueExp, boolean allowCallingFunctions,
+ ASTExpression[] positionalArgs, NamedArgument[] namedArgs, String[] loopVarNames,
+ TemplateElements children) {
+ this.callableValueExp = callableValueExp;
+ this.allowCallingFunctions = allowCallingFunctions;
+
+ this.positionalArgs = positionalArgs;
+ this.namedArgs = namedArgs;
+ this.loopVarNames = loopVarNames;
+
+ setChildren(children);
+ }
+
+ @Override
+ ASTElement[] accept(Environment env) throws TemplateException, IOException {
+ TemplateCallableModel callableValue;
+ TemplateDirectiveModel2 directive;
+ TemplateFunctionModel function;
+ {
+ TemplateModel callableValueTM = callableValueExp._eval(env);
+ if (callableValueTM instanceof TemplateDirectiveModel2) {
+ callableValue = (TemplateCallableModel) callableValueTM;
+ directive = (TemplateDirectiveModel2) callableValueTM;
+ function = null;
+ } else if (callableValueTM instanceof TemplateFunctionModel) {
+ if (!allowCallingFunctions) {
+ // TODO [FM3][CF] Better exception
+ throw new NonUserDefinedDirectiveLikeException(
+ "Calling functions is not allowed on the top level in this template language", env);
+ }
+ callableValue = (TemplateCallableModel) callableValueTM;
+ directive = null;
+ function = (TemplateFunctionModel) callableValue;
+ } else if (callableValueTM == null) {
+ throw InvalidReferenceException.getInstance(callableValueExp, env);
+ } else {
+ throw new NonUserDefinedDirectiveLikeException(callableValueExp, callableValueTM, env);
+ }
+ }
+
+ int predefPosArgCnt = callableValue.getPredefinedPositionalArgumentCount();
+ boolean hasPosVarargsArg = callableValue.hasPositionalVarargsArgument();
+
+ if (positionalArgs != null && positionalArgs.length > predefPosArgCnt && !hasPosVarargsArg) {
+ // TODO [FM3][CF] Better exception
+ throw new _MiscTemplateException(this,
+ "The target callable ",
+ (predefPosArgCnt != 0
+ ? new Object[] { "can only have ", predefPosArgCnt }
+ : "can't have"
+ ),
+ " arguments passed by position, but the invocation has ",
+ positionalArgs.length, " such arguments.");
+ }
+
+ TemplateModel[] execArgs = new TemplateModel[callableValue.getTotalArgumentCount()];
+
+ // Fill predefined positional args:
+ if (positionalArgs != null) {
+ int actualPredefPosArgCnt = Math.min(positionalArgs.length, predefPosArgCnt);
+ for (int argIdx = 0; argIdx < actualPredefPosArgCnt; argIdx++) {
+ execArgs[argIdx] = positionalArgs[argIdx].eval(env);
+ }
+ }
+
+ if (hasPosVarargsArg) {
+ int posVarargCnt = positionalArgs != null ? positionalArgs.length - predefPosArgCnt : 0;
+ TemplateSequenceModel varargsSeq;
+ if (posVarargCnt <= 0) {
+ varargsSeq = Constants.EMPTY_SEQUENCE;
+ } else {
+ NativeSequence nativeSeq = new NativeSequence(posVarargCnt);
+ varargsSeq = nativeSeq;
+ for (int posVarargIdx = 0; posVarargIdx < posVarargCnt; posVarargIdx++) {
+ nativeSeq.add(positionalArgs[predefPosArgCnt + posVarargIdx].eval(env));
+ }
+ }
+ execArgs[predefPosArgCnt] = varargsSeq;
+ }
+
+ int namedVarargsArgumentIndex = callableValue.getNamedVarargsArgumentIndex();
+ NativeHashEx2 namedVarargsHash = null;
+ if (namedArgs != null) {
+ for (NamedArgument namedArg : namedArgs) {
+ int argIdx = callableValue.getNamedArgumentIndex(namedArg.name);
+ if (argIdx != -1) {
+ execArgs[argIdx] = namedArg.value.eval(env);
+ } else {
+ if (namedVarargsHash == null) {
+ if (namedVarargsArgumentIndex == -1) {
+ Collection<String> validNames = callableValue.getPredefinedNamedArgumentNames();
+ throw new _MiscTemplateException(this,
+ // TODO [FM3][CF] Better exception, esp. list the supported names
+ validNames == null || validNames.isEmpty()
+ ? new Object[] {
+ "The target callable doesn't have any by-name-passed parameters (like ",
+ new _DelayedJQuote(namedArg.name), ")"
+ }
+ : new Object[] {
+ "The target callable has no by-name-passed parameter called ",
+ new _DelayedJQuote(namedArg.name), ". The supported parameter names are:\n",
+ new _DelayedJQuotedListing(validNames)
+ });
+ }
+ namedVarargsHash = new NativeHashEx2();
+ }
+ namedVarargsHash.put(namedArg.name, namedArg.value.eval(env));
+ }
+ }
+ }
+ if (namedVarargsArgumentIndex != -1) {
+ execArgs[namedVarargsArgumentIndex] = namedVarargsHash != null ? namedVarargsHash : Constants.EMPTY_HASH;
+ }
+
+ if (directive != null) {
+ directive.execute(execArgs, env.getOut(), env, this);
+ } else {
+ TemplateModel result = function.execute(execArgs, env, this);
+ if (result == null) {
+ throw new _MiscTemplateException(this, "Function has returned no value (or null)");
+ }
+ // TODO [FM3][CF]
+ throw new BugException("Top-level function call not yet implemented");
+ }
+
+ return null;
+ }
+
+ @Override
+ boolean isNestedBlockRepeater() {
+ return true;
+ }
+
+ @Override
+ boolean isShownInStackTrace() {
+ return true;
+ }
+
+ @Override
+ protected String dump(boolean canonical) {
+ StringBuilder sb = new StringBuilder();
+ if (canonical) sb.append('<');
+ sb.append('~');
+ MessageUtil.appendExpressionAsUntearable(sb, callableValueExp);
+ boolean nameIsInParen = sb.charAt(sb.length() - 1) == ')';
+ if (positionalArgs != null) {
+ for (int i = 0; i < positionalArgs.length; i++) {
+ ASTExpression argExp = (ASTExpression) positionalArgs[i];
+ if (i != 0) {
+ sb.append(',');
+ }
+ sb.append(' ');
+ sb.append(argExp.getCanonicalForm());
+ }
+ }
+ if (namedArgs != null) {
+ for (NamedArgument namedArg : namedArgs) {
+ sb.append(' ');
+ sb.append(_StringUtil.toFTLTopLevelIdentifierReference(namedArg.name));
+ sb.append('=');
+ MessageUtil.appendExpressionAsUntearable(sb, namedArg.value);
+ }
+ }
+ if (loopVarNames != null && loopVarNames.length != 0) {
+ sb.append("; ");
+ for (int i = 0; i < loopVarNames.length; i++) {
+ if (i != 0) {
+ sb.append(", ");
+ }
+ sb.append(_StringUtil.toFTLTopLevelIdentifierReference((String) loopVarNames[i]));
+ }
+ }
+ if (canonical) {
+ if (getChildCount() == 0) {
+ sb.append("/>");
+ } else {
+ sb.append('>');
+ sb.append(getChildrenCanonicalForm());
+ sb.append("</~");
+ if (!nameIsInParen
+ && (callableValueExp instanceof ASTExpVariable
+ || (callableValueExp instanceof ASTExpDot && ((ASTExpDot) callableValueExp).onlyHasIdentifiers()))) {
+ sb.append(callableValueExp.getCanonicalForm());
+ }
+ sb.append('>');
+ }
+ }
+ return sb.toString();
+ }
+
+ @Override
+ String getASTNodeDescriptor() {
+ return "~";
+ }
+
+ @Override
+ int getParameterCount() {
+ return 1/*nameExp*/
+ + (positionalArgs != null ? positionalArgs.length : 0)
+ + (namedArgs != null ? namedArgs.length * 2 : 0)
+ + (loopVarNames != null ? loopVarNames.length : 0);
+ }
+
+ @Override
+ Object getParameterValue(int idx) {
+ if (idx == 0) {
+ return callableValueExp;
+ } else {
+ int base = 1;
+ final int positionalArgsSize = positionalArgs != null ? positionalArgs.length : 0;
+ if (idx - base < positionalArgsSize) {
+ return positionalArgs[idx - base];
+ } else {
+ base += positionalArgsSize;
+ final int namedArgsSize = namedArgs != null ? namedArgs.length : 0;
+ if (idx - base < namedArgsSize * 2) {
+ NamedArgument namedArg = namedArgs[(idx - base) / 2];
+ return (idx - base) % 2 == 0 ? namedArg.name : namedArg.value;
+ } else {
+ base += namedArgsSize * 2;
+ final int bodyParameterNamesSize = loopVarNames != null ? loopVarNames.length : 0;
+ if (idx - base < bodyParameterNamesSize) {
+ return loopVarNames[idx - base];
+ } else {
+ throw new IndexOutOfBoundsException();
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ ParameterRole getParameterRole(int idx) {
+ if (idx == 0) {
+ return ParameterRole.CALLEE;
+ } else {
+ int base = 1;
+ final int positionalArgsSize = positionalArgs != null ? positionalArgs.length : 0;
+ if (idx - base < positionalArgsSize) {
+ return ParameterRole.ARGUMENT_VALUE;
+ } else {
+ base += positionalArgsSize;
+ final int namedArgsSize = namedArgs != null ? namedArgs.length : 0;
+ if (idx - base < namedArgsSize * 2) {
+ return (idx - base) % 2 == 0 ? ParameterRole.ARGUMENT_NAME : ParameterRole.ARGUMENT_VALUE;
+ } else {
+ base += namedArgsSize * 2;
+ final int bodyParameterNamesSize = loopVarNames != null ? loopVarNames.length : 0;
+ if (idx - base < bodyParameterNamesSize) {
+ return ParameterRole.TARGET_LOOP_VARIABLE;
+ } else {
+ throw new IndexOutOfBoundsException();
+ }
+ }
+ }
+ }
+ }
+
+ // -----------------------------------------------------------------------------------------------------------------
+ // CallPlace API:
+
+ @Override
+ public boolean hasNestedContent() {
+ return getChildCount() != 0;
+ }
+
+ @Override
+ public int getLoopVariableCount() {
+ return loopVarNames != null ? loopVarNames.length : 0;
+ }
+
+ @Override
+ public void executeNestedContent(TemplateModel[] loopVariableValues, Environment env) throws TemplateException {
+ // TODO Automatically generated method
+ throw new BugException("Not implemented");
+ }
+
+ @Override
+ @SuppressFBWarnings(value={ "IS2_INCONSISTENT_SYNC", "DC_DOUBLECHECK" }, justification="Performance tricks")
+ public Object getOrCreateCustomData(Object providerIdentity, CommonSupplier supplier)
+ throws CallPlaceCustomDataInitializationException {
+ // We are using double-checked locking, utilizing Java memory model "final" trick.
+ // Note that this.customDataHolder is NOT volatile.
+
+ CustomDataHolder customDataHolder = this.customDataHolder; // Findbugs false alarm
+ if (customDataHolder == null) { // Findbugs false alarm
+ synchronized (this) {
+ customDataHolder = this.customDataHolder;
+ if (customDataHolder == null || customDataHolder.providerIdentity != providerIdentity) {
+ customDataHolder = createNewCustomData(providerIdentity, supplier);
+ this.customDataHolder = customDataHolder;
+ }
+ }
+ }
+
+ if (customDataHolder.providerIdentity != providerIdentity) {
+ synchronized (this) {
+ customDataHolder = this.customDataHolder;
+ if (customDataHolder == null || customDataHolder.providerIdentity != providerIdentity) {
+ customDataHolder = createNewCustomData(providerIdentity, supplier);
+ this.customDataHolder = customDataHolder;
+ }
+ }
+ }
+
+ return customDataHolder.customData;
+ }
+
+ private CustomDataHolder createNewCustomData(Object provierIdentity, CommonSupplier supplier)
+ throws CallPlaceCustomDataInitializationException {
+ CustomDataHolder customDataHolder;
+ Object customData;
+ try {
+ customData = supplier.get();
+ } catch (Exception e) {
+ throw new CallPlaceCustomDataInitializationException(
+ "Failed to initialize custom data for provider identity "
+ + _StringUtil.tryToString(provierIdentity) + " via factory "
+ + _StringUtil.tryToString(supplier), e);
+ }
+ if (customData == null) {
+ throw new NullPointerException("CommonSupplier.get() has returned null");
+ }
+ customDataHolder = new CustomDataHolder(provierIdentity, customData);
+ return customDataHolder;
+ }
+
+ @Override
+ public boolean isNestedOutputCacheable() {
+ return isChildrenOutputCacheable();
+ }
+
+ @Override
+ public int getFirstTargetJavaParameterTypeIndex() {
+ // TODO [FM3]
+ return -1;
+ }
+
+ @Override
+ public Class<?> getTargetJavaParameterType(int argIndex) {
+ // TODO [FM3]
+ return null;
+ }
+
+ /**
+ * Used for implementing double check locking in implementing the
+ * {@link #getOrCreateCustomData(Object, CommonSupplier)}.
+ */
+ private static class CustomDataHolder {
+
+ private final Object providerIdentity;
+ private final Object customData;
+ public CustomDataHolder(Object providerIdentity, Object customData) {
+ this.providerIdentity = providerIdentity;
+ this.customData = customData;
+ }
+
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/freemarker-core/src/main/java/org/apache/freemarker/core/NonNumericalException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/NonNumericalException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/NonNumericalException.java
index f70bd83..1b9b586 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/NonNumericalException.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/NonNumericalException.java
@@ -19,6 +19,8 @@
package org.apache.freemarker.core;
+import java.io.Serializable;
+
import org.apache.freemarker.core.model.TemplateModel;
import org.apache.freemarker.core.model.TemplateNumberModel;
@@ -28,7 +30,7 @@ import org.apache.freemarker.core.model.TemplateNumberModel;
public class NonNumericalException extends UnexpectedTypeException {
private static final Class[] EXPECTED_TYPES = new Class[] { TemplateNumberModel.class };
-
+
public NonNumericalException(Environment env) {
super(env, "Expecting numerical value here");
}
@@ -64,6 +66,14 @@ public class NonNumericalException extends UnexpectedTypeException {
throws InvalidReferenceException {
super(assignmentTargetVarName, model, "number", EXPECTED_TYPES, tips, env);
}
+
+
+ NonNumericalException(
+ Serializable argumentNameOrIndex, TemplateModel model, String[] tips, Environment env)
+ throws InvalidReferenceException {
+ super(argumentNameOrIndex, model, "number", EXPECTED_TYPES, tips, env);
+ }
+
static NonNumericalException newMalformedNumberException(ASTExpression blamed, String text, Environment env) {
return new NonNumericalException(
new _ErrorDescriptionBuilder("Can't convert this string to number: ", new _DelayedJQuote(text))
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/freemarker-core/src/main/java/org/apache/freemarker/core/NonUserDefinedDirectiveLikeException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/NonUserDefinedDirectiveLikeException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/NonUserDefinedDirectiveLikeException.java
index 4cf353b..1daae3e 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/NonUserDefinedDirectiveLikeException.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/NonUserDefinedDirectiveLikeException.java
@@ -23,6 +23,7 @@ import org.apache.freemarker.core.model.TemplateDirectiveModel;
import org.apache.freemarker.core.model.TemplateModel;
import org.apache.freemarker.core.model.TemplateTransformModel;
+// TODO [FM3][CF] Review and rename this when TDM2 and TFM are in place
/**
* Indicates that a {@link TemplateDirectiveModel} or {@link TemplateTransformModel} or {@link ASTDirMacro} value was
* expected, but the value had a different type.
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateCallableModelUtils.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateCallableModelUtils.java b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateCallableModelUtils.java
new file mode 100644
index 0000000..4ee9f77
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/TemplateCallableModelUtils.java
@@ -0,0 +1,48 @@
+package org.apache.freemarker.core;
+
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+
+public class TemplateCallableModelUtils {
+
+ public static final TemplateModel[] EMPTY_TEMPLATE_MODEL_ARRAY = new TemplateModel[0];
+
+ // TODO [FM3][CF] Add this to the other exception classes too
+ public static TemplateNumberModel castArgumentToNumber(TemplateModel[] args, int argIndex, boolean allowNull,
+ Environment env) throws TemplateException {
+ return getTemplateNumberModel(args[argIndex], argIndex, allowNull, env);
+ }
+
+ // TODO [FM3][CF] Add this to the other exception classes too
+ private static TemplateNumberModel getTemplateNumberModel(TemplateModel argValue, int argIndex, boolean allowNull,
+ Environment env) throws TemplateException {
+ if (argValue instanceof TemplateNumberModel) {
+ return (TemplateNumberModel) argValue;
+ }
+ if (argValue == null) {
+ if (allowNull) {
+ return null;
+ }
+ throw new _MiscTemplateException(env,
+ "The ", new _DelayedOrdinal(argIndex + 1), " argument can't be null.");
+ }
+ throw new NonNumericalException(argIndex, argValue, null, env);
+ }
+
+ // TODO [FM3][CF] Add this to the other exception classes too
+ public static TemplateNumberModel castArgumentToNumber(TemplateModel argValue, String argName, boolean allowNull,
+ Environment env) throws TemplateException {
+ if (argValue instanceof TemplateNumberModel) {
+ return (TemplateNumberModel) argValue;
+ }
+ if (argValue == null) {
+ if (allowNull) {
+ return null;
+ }
+ throw new _MiscTemplateException(env,
+ "The ", new _DelayedJQuote(argName), " argument can't be null.");
+ }
+ throw new NonNumericalException(argName, argValue, null, env);
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/freemarker-core/src/main/java/org/apache/freemarker/core/UnexpectedTypeException.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/UnexpectedTypeException.java b/freemarker-core/src/main/java/org/apache/freemarker/core/UnexpectedTypeException.java
index 939d8d2..741eed0 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/UnexpectedTypeException.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/UnexpectedTypeException.java
@@ -19,6 +19,8 @@
package org.apache.freemarker.core;
+import java.io.Serializable;
+
import org.apache.freemarker.core.model.TemplateModel;
/**
@@ -37,14 +39,16 @@ public class UnexpectedTypeException extends TemplateException {
UnexpectedTypeException(
ASTExpression blamed, TemplateModel model, String expectedTypesDesc, Class[] expectedTypes, Environment env)
throws InvalidReferenceException {
- super(null, env, blamed, newDesciptionBuilder(blamed, null, model, expectedTypesDesc, expectedTypes, env));
+ super(null, env, blamed, newDescriptionBuilder(blamed, null, null, model, expectedTypesDesc, expectedTypes,
+ env));
}
UnexpectedTypeException(
ASTExpression blamed, TemplateModel model, String expectedTypesDesc, Class[] expectedTypes, String tip,
Environment env)
throws InvalidReferenceException {
- super(null, env, blamed, newDesciptionBuilder(blamed, null, model, expectedTypesDesc, expectedTypes, env)
+ super(null, env, blamed, newDescriptionBuilder(blamed, null, null, model, expectedTypesDesc, expectedTypes,
+ env)
.tip(tip));
}
@@ -52,32 +56,47 @@ public class UnexpectedTypeException extends TemplateException {
ASTExpression blamed, TemplateModel model, String expectedTypesDesc, Class[] expectedTypes, Object[] tips,
Environment env)
throws InvalidReferenceException {
- super(null, env, blamed, newDesciptionBuilder(blamed, null, model, expectedTypesDesc, expectedTypes, env)
+ super(null, env, blamed, newDescriptionBuilder(blamed, null, null, model, expectedTypesDesc, expectedTypes, env)
.tips(tips));
}
+ /**
+ * Used for assignments that use {@code +=} and such.
+ */
UnexpectedTypeException(
String blamedAssignmentTargetVarName, TemplateModel model, String expectedTypesDesc, Class[] expectedTypes,
Object[] tips,
Environment env)
throws InvalidReferenceException {
- super(null, env, null, newDesciptionBuilder(
- null, blamedAssignmentTargetVarName, model, expectedTypesDesc, expectedTypes, env).tips(tips));
+ super(null, env, null, newDescriptionBuilder(
+ null, blamedAssignmentTargetVarName, null, model, expectedTypesDesc, expectedTypes, env).tips(tips));
}
-
+
/**
- * @param blamedAssignmentTargetVarName
- * Used for assignments that use {@code +=} and such, in which case the {@code blamed} expression
- * parameter will be null {@code null} and this parameter will be non-{null}.
+ * Used when the value of a directive/function argument has a different type than that the directive/function
+ * expects.
*/
- private static _ErrorDescriptionBuilder newDesciptionBuilder(
- ASTExpression blamed, String blamedAssignmentTargetVarName,
+ UnexpectedTypeException(
+ Serializable blamedArgumentNameOrIndex, TemplateModel model, String expectedTypesDesc, Class[] expectedTypes,
+ Object[] tips,
+ Environment env)
+ throws InvalidReferenceException {
+ super(null, env, null, newDescriptionBuilder(
+ null, null, blamedArgumentNameOrIndex, model, expectedTypesDesc, expectedTypes, env).tips(tips));
+ }
+
+ private static _ErrorDescriptionBuilder newDescriptionBuilder(
+ ASTExpression blamed, String blamedAssignmentTargetVarName, Serializable blamedArgumentNameOrIndex,
TemplateModel model, String expectedTypesDesc, Class[] expectedTypes, Environment env)
throws InvalidReferenceException {
- if (model == null) throw InvalidReferenceException.getInstance(blamed, env);
+ if (model == null) {
+ throw InvalidReferenceException.getInstance(blamed, env);
+ }
_ErrorDescriptionBuilder errorDescBuilder = new _ErrorDescriptionBuilder(
- unexpectedTypeErrorDescription(expectedTypesDesc, blamed, blamedAssignmentTargetVarName, model))
+ unexpectedTypeErrorDescription(expectedTypesDesc,
+ blamed, blamedAssignmentTargetVarName, blamedArgumentNameOrIndex,
+ model))
.blame(blamed).showBlamer(true);
if (model instanceof _UnexpectedTypeErrorExplainerTemplateModel) {
Object[] tip = ((_UnexpectedTypeErrorExplainerTemplateModel) model).explainTypeError(expectedTypes);
@@ -90,15 +109,25 @@ public class UnexpectedTypeException extends TemplateException {
private static Object[] unexpectedTypeErrorDescription(
String expectedTypesDesc,
- ASTExpression blamed, String blamedAssignmentTargetVarName,
+ ASTExpression blamed, String blamedAssignmentTargetVarName, Serializable blamedArgumentNameOrIndex,
TemplateModel model) {
return new Object[] {
- "Expected ", new _DelayedAOrAn(expectedTypesDesc), ", but ",
- (blamedAssignmentTargetVarName == null
- ? blamed != null ? "this" : "the expression"
- : new Object[] {
- "assignment target variable ",
- new _DelayedJQuote(blamedAssignmentTargetVarName) }),
+ "Expected ", new _DelayedAOrAn(expectedTypesDesc), ", but ", (
+ blamedAssignmentTargetVarName != null
+ ? new Object[] {
+ "assignment target variable ",
+ new _DelayedJQuote(blamedAssignmentTargetVarName) }
+ : blamedArgumentNameOrIndex != null
+ ? new Object[] {
+ "the ",
+ (blamedArgumentNameOrIndex instanceof Integer
+ ? new _DelayedOrdinal(((Integer) blamedArgumentNameOrIndex) + 1)
+ : new _DelayedJQuote(blamedArgumentNameOrIndex)),
+ " argument"}
+ : blamed != null
+ ? "this"
+ : "the expression"
+ ),
" has evaluated to ",
new _DelayedAOrAn(new _DelayedFTLTypeDescription(model)),
(blamedAssignmentTargetVarName == null ? ":" : ".")};
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedJQuotedListing.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedJQuotedListing.java b/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedJQuotedListing.java
new file mode 100644
index 0000000..e809a92
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/_DelayedJQuotedListing.java
@@ -0,0 +1,46 @@
+/*
+ * 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 org.apache.freemarker.core;
+
+import java.util.Collection;
+
+import org.apache.freemarker.core.util._StringUtil;
+
+/** Don't use this; used internally by FreeMarker, might changes without notice. */
+public class _DelayedJQuotedListing extends _DelayedConversionToString {
+
+ public _DelayedJQuotedListing(Collection<?> object) {
+ super(object);
+ }
+
+ @Override
+ protected String doConversion(Object obj) {
+ StringBuilder sb = new StringBuilder();
+ for (Object element : (Collection<?>) obj) {
+ if (sb.length() != 0) {
+ sb.append(", ");
+ }
+ sb.append(_StringUtil.jQuote(element));
+ }
+
+ return sb.toString();
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/freemarker-core/src/main/java/org/apache/freemarker/core/model/CallPlace.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/CallPlace.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/CallPlace.java
new file mode 100644
index 0000000..1f131a3
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/CallPlace.java
@@ -0,0 +1,173 @@
+/*
+ * 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 org.apache.freemarker.core.model;
+
+import java.util.IdentityHashMap;
+
+import org.apache.freemarker.core.CallPlaceCustomDataInitializationException;
+import org.apache.freemarker.core.DirectiveCallPlace;
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.Template;
+import org.apache.freemarker.core.TemplateCallableModelUtils;
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.core.util.CommonSupplier;
+
+/**
+ * The place in a template from where a directive (like a macro) or function (like a method) is called;
+ * <b>Do not implement this interface yourself</b>, as new methods may be added any time! Only FreeMarker itself
+ * should provide implementations.
+ */
+// TODO [FM3][CF] Should also replace DirectiveCallPlace
+public interface CallPlace {
+
+ // -------------------------------------------------------------------------------------------------------------
+ // Nested content:
+
+ /**
+ * Tells if there's a non-zero-length nested content. This is {@code false} for {@code <@foo />} or
+ * {@code <@f...@foo>} or for calls inside expressions (i.e., for function calls).
+ */
+ boolean hasNestedContent();
+
+ /**
+ * The number of loop variables specified for this call.
+ */
+ int getLoopVariableCount();
+
+ /**
+ * Executed the nested content; it there's none, it just does nothing.
+ *
+ * @param loopVariableValues
+ * The loop variables to pass to the nested content; not {@code null} (use {@link
+ * TemplateCallableModelUtils#EMPTY_TEMPLATE_MODEL_ARRAY}. Its length must be equal to
+ * {@link #getLoopVariableCount()}.
+ */
+ void executeNestedContent(TemplateModel[] loopVariableValues, Environment env) throws TemplateException;
+
+ // -------------------------------------------------------------------------------------------------------------
+ // Source code info:
+
+ /**
+ * The template that contains this call.
+ */
+ Template getTemplate();
+
+ /**
+ * The 1-based column number of the first character of the directive call in the template source code, or -1 if it's
+ * not known.
+ */
+ int getBeginColumn();
+
+ /**
+ * The 1-based line number of the first character of the directive call in the template source code, or -1 if it's
+ * not known.
+ */
+ int getBeginLine();
+
+ /**
+ * The 1-based column number of the last character of the directive call in the template source code, or -1 if it's
+ * not known. If the directive has an end-tag ({@code </...@...>}), then it points to the last character of that.
+ */
+ int getEndColumn();
+
+ /**
+ * The 1-based line number of the last character of the directive call in the template source code, or -1 if it's
+ * not known. If the directive has an end-tag ({@code </...@...>}), then it points to the last character of that.
+ */
+ int getEndLine();
+
+ // -------------------------------------------------------------------------------------------------------------
+ // Caching:
+
+ /**
+ * Returns the custom data, or if that's {@code null}, then it creates and stores it in an atomic operation then
+ * returns it. This method is thread-safe, however, it doesn't ensure thread safe (like synchronized) access to the
+ * custom data itself. See the top-level documentation of {@link DirectiveCallPlace} to understand the scope and
+ * life-cycle of the custom data. Be sure that the custom data only depends on things that get their final value
+ * during template parsing, not on runtime settings.
+ *
+ * <p>
+ * This method will block other calls while the {@code supplier} is executing, thus, the object will be
+ * <em>usually</em> created only once, even if multiple threads request the value when it's still {@code null}. It
+ * doesn't stand though when {@code providerIdentity} mismatches occur (see later). Furthermore, then it's also
+ * possible that multiple objects created by the same {@link CommonSupplier} will be in use on the same time, because
+ * of directive executions already running in parallel, and because of memory synchronization delays (hardware
+ * dependent) between the threads.
+ *
+ * @param providerIdentity
+ * This is usually the class of the {@link TemplateDirectiveModel} that creates (and uses) the custom
+ * data, or if you are using your own class for the custom data object (as opposed to a class from some
+ * more generic API), then that class. This is needed as the same call place might calls different
+ * directives depending on runtime conditions, and so it must be ensured that these directives won't
+ * accidentally read each other's custom data, ending up with class cast exceptions or worse. In the
+ * current implementation, if there's a {@code providerIdentity} mismatch (means, the
+ * {@code providerIdentity} object used when the custom data was last set isn't the exactly same object
+ * as the one provided with the parameter now), the previous custom data will be just ignored as if it
+ * was {@code null}. So if multiple directives that use the custom data feature use the same call place,
+ * the caching of the custom data can be inefficient, as they will keep overwriting each other's custom
+ * data. (In a more generic implementation the {@code providerIdentity} would be a key in a
+ * {@link IdentityHashMap}, but then this feature would be slower, while {@code providerIdentity}
+ * mismatches aren't occurring in most applications.)
+ * @param supplier
+ * Called when the custom data wasn't yet set, to invoke its initial value. If this parameter is
+ * {@code null} and the custom data wasn't set yet, then {@code null} will be returned. The returned
+ * value of {@link CommonSupplier#get()} can be any kind of object, but can't be {@code null}.
+ *
+ * @return The current custom data object, or possibly {@code null} if there was no {@link CommonSupplier} provided.
+ *
+ * @throws CallPlaceCustomDataInitializationException
+ * If the {@link CommonSupplier} had to be invoked but failed.
+ */
+ Object getOrCreateCustomData(Object providerIdentity, CommonSupplier supplier)
+ throws CallPlaceCustomDataInitializationException;
+
+ /**
+ * Tells if the nested content (the body) can be safely cached, as it only depends on the template content (not on
+ * variable values and such) and has no side-effects (other than writing to the output). Examples of cases that give
+ * {@code false}: {@code <@foo>Name: } <tt...@foo>},
+ * {@code <@foo>Name: <#if showIt>Joe</#...@foo>}. Examples of cases that give {@code true}:
+ * {@code <@foo>Name: Joe</...@foo>}, {@code <@foo />}. Note that we get {@code true} for no nested content, because
+ * that's equivalent to 0-length nested content.
+ * <p>
+ * This method returns a pessimistic result. For example, if it sees a custom directive call, it can't know what it
+ * does, so it will assume that it's not cacheable.
+ */
+ boolean isNestedOutputCacheable();
+
+ // -------------------------------------------------------------------------------------------------------------
+ // Miscellaneous:
+
+ /**
+ * Used solely for speed optimization (to minimize the number of
+ * {@link #getTargetJavaParameterType(int)} calls).
+ *
+ * @return -1 if no parameter has type hint
+ */
+ int getFirstTargetJavaParameterTypeIndex();
+
+ /**
+ * The type of the parameter in the target Java method; used for overloaded Java method selection.
+ * This optional information is specified by the template author in the source code.
+ *
+ * @return The desired type or {@code null} if this information wasn't specified in the template.
+ */
+ Class<?> getTargetJavaParameterType(int argIndex);
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/freemarker-core/src/main/java/org/apache/freemarker/core/model/Constants.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/Constants.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/Constants.java
index 268188d..f72d3e6 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/model/Constants.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/Constants.java
@@ -20,7 +20,9 @@
package org.apache.freemarker.core.model;
import java.io.Serializable;
+import java.util.NoSuchElementException;
+import org.apache.freemarker.core.model.TemplateHashModelEx2.KeyValuePairIterator;
import org.apache.freemarker.core.model.impl.SimpleNumber;
/**
@@ -47,7 +49,7 @@ public class Constants {
public static final TemplateNumberModel MINUS_ONE = new SimpleNumber(-1);
public static final TemplateModelIterator EMPTY_ITERATOR = new EmptyIteratorModel();
-
+
private static class EmptyIteratorModel implements TemplateModelIterator, Serializable {
@Override
@@ -101,7 +103,7 @@ public class Constants {
public static final TemplateHashModelEx EMPTY_HASH = new EmptyHashModel();
- private static class EmptyHashModel implements TemplateHashModelEx, Serializable {
+ private static class EmptyHashModel implements TemplateHashModelEx2, Serializable {
@Override
public int size() throws TemplateModelException {
@@ -127,7 +129,33 @@ public class Constants {
public boolean isEmpty() throws TemplateModelException {
return true;
}
-
+
+ @Override
+ public KeyValuePairIterator keyValuePairIterator() throws TemplateModelException {
+ return EMPTY_KEY_VALUE_PAIR_ITERATOR;
+ }
}
-
+
+ public static final KeyValuePairIterator EMPTY_KEY_VALUE_PAIR_ITERATOR = EmptyKeyValuePairIterator.INSTANCE;
+
+ private static class EmptyKeyValuePairIterator implements TemplateHashModelEx2.KeyValuePairIterator {
+
+ static final EmptyKeyValuePairIterator INSTANCE = new EmptyKeyValuePairIterator();
+
+ private EmptyKeyValuePairIterator() {
+ //
+ }
+
+ @Override
+ public boolean hasNext() throws TemplateModelException {
+ return false;
+ }
+
+ @Override
+ public TemplateHashModelEx2.KeyValuePair next() throws TemplateModelException {
+ throw new NoSuchElementException("Can't retrieve element from empty key-value pair iterator.");
+ }
+
+ }
+
}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateCallableModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateCallableModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateCallableModel.java
new file mode 100644
index 0000000..bc053bd
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateCallableModel.java
@@ -0,0 +1,86 @@
+/*
+ * 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 org.apache.freemarker.core.model;
+
+import java.util.Collection;
+
+/**
+ * Super interface of {@link TemplateFunctionModel} and {@link TemplateDirectiveModel2}.
+ */
+public interface TemplateCallableModel extends TemplateModel {
+
+ // -------------------------------------------------------------------------------------------------------------
+ // Arguments:
+
+ /**
+ * The highest allowed number of positional arguments, not counting the positional varargs argument. The actual
+ * positional argument count can be less than this if there are optional positional argument. When calling the
+ * {@code execute} method, this many items will be reserved for the positional arguments in the argument array (not
+ * counting the item for the positional varargs argument, if there's one). Positional arguments
+ * above this count will go to the varargs argument (if there's one, otherwise it's an error).
+ */
+ int getPredefinedPositionalArgumentCount();
+
+ /**
+ * Tells if there's no position varargs argument. If there is, then it must be in the argument array at the
+ * index equals to {@link #getPredefinedPositionalArgumentCount()}. The positional varargs argument is a
+ * {@link TemplateSequenceModel} that collects all positional arguments whose index would be greater than
+ * or equal to {@link #getPredefinedPositionalArgumentCount()}.
+ */
+ boolean hasPositionalVarargsArgument();
+
+ /**
+ * Returns if with what array index should the given named argument by passed to the {@code execute} method.
+ *
+ * @return -1 if there's no such named argument
+ */
+ int getNamedArgumentIndex(String name);
+
+ /**
+ * Returns the index of the named varargs argument in the argument array, or -1 if there's no named varargs
+ * argument. The named varargs argument is a {@link TemplateHashModelEx2} with string keys that collects all
+ * the named arguments for which {@link #getNamedArgumentIndex(String)} returns -1. The iteration order of this
+ * hash follows the order in which the arguments were specified in the calling template.
+ *
+ * @return -1 if there's no named varargs argument
+ */
+ int getNamedVarargsArgumentIndex();
+
+ /**
+ * The required length of the arguments array passed to the {@code execute} method. (It's possible that a longer
+ * array will be passed, in which case the extra elements should be ignored by {@code execute}.)
+ * The return value should be equal to the sum of these (but we don't want to calculate it on-the-fly, for speed),
+ * or else FreeMarker might fails later with {@link IndexOutOfBoundsException}:
+ * <ul>
+ * <li>{@link #getPredefinedPositionalArgumentCount()}
+ * <li>If {@link #hasPositionalVarargsArgument()} is {@code true}, then 1, else 0.
+ * <li>Size of {@link #getPredefinedNamedArgumentNames()}
+ * <li>If {@link #getNamedVarargsArgumentIndex()} is not -1, then 1, else 0. (Also, obviously, if
+ * {@link #getNamedVarargsArgumentIndex()} is not -1, then it's one less than the return value of this method.)
+ * </ul>
+ */
+ int getTotalArgumentCount();
+
+ /**
+ * The valid named argument names in the order as they should be displayed in error messages, or {@code null}.
+ */
+ Collection<String> getPredefinedNamedArgumentNames();
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateDirectiveModel2.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateDirectiveModel2.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateDirectiveModel2.java
new file mode 100644
index 0000000..e430639
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateDirectiveModel2.java
@@ -0,0 +1,28 @@
+package org.apache.freemarker.core.model;
+
+import java.io.IOException;
+import java.io.Writer;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.TemplateException;
+
+/**
+ * A {@link TemplateCallableModel} that (progressively) prints it result into the {@code out} object, instead of
+ * returning a single result at the end of the execution. Many of these won't print anything, but has other
+ * side-effects why it's useful for calling them. When used in an expression context, the printer output will be the
+ * value of the call (which depending on the output format of the directive is a {@link TemplateMarkupOutputModel},
+ * or a {@link String}).
+ */
+// TODO [FM3][CF] Rename this to TemplateDirectiveModel
+public interface TemplateDirectiveModel2 extends TemplateCallableModel {
+
+ /**
+ * @param args Array with {@link #getTotalArgumentCount()} elements (or more, in which case the extra elements
+ * should be ignored). If a parameter was omitted, the corresponding array element will be {@code null}.
+ */
+ void execute(
+ TemplateModel[] args, Writer out,
+ Environment env, CallPlace callPlace)
+ throws TemplateException, IOException;
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateFunctionModel.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateFunctionModel.java b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateFunctionModel.java
new file mode 100644
index 0000000..7944d81
--- /dev/null
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/model/TemplateFunctionModel.java
@@ -0,0 +1,25 @@
+package org.apache.freemarker.core.model;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.ProcessingConfiguration;
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.core.outputformat.OutputFormat;
+
+/**
+ * A {@link TemplateCallableModel}, which returns its result as a {@link TemplateModel} at the end of the execution.
+ * This is in contrast with {@link TemplateDirectiveModel2}, which writes its result progressively to the output.
+ *
+ * <p>Some template languages may allow function calls directly embedded into static text, as in
+ * <code>text#f()text</code>. In that case, the language has to ensure that the return value is formatted according
+ * the {@link ProcessingConfiguration} settings (such as {@link ProcessingConfiguration#getNumberFormat()} and
+ * {@link ProcessingConfiguration#getDateFormat()}), and is printed to the output after escaping according the
+ * {@link OutputFormat} of the context. Some template languages instead require using an explicit expression value
+ * printer statement, as in <code>text${f()}text</code>.
+ */
+public interface TemplateFunctionModel extends TemplateCallableModel {
+
+ TemplateModel execute(
+ TemplateModel[] args, Environment env, CallPlace callPlace)
+ throws TemplateException;
+
+}
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/c28a78bd/freemarker-core/src/main/java/org/apache/freemarker/core/util/FTLUtil.java
----------------------------------------------------------------------
diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/util/FTLUtil.java b/freemarker-core/src/main/java/org/apache/freemarker/core/util/FTLUtil.java
index 0752f1b..1da4f62 100644
--- a/freemarker-core/src/main/java/org/apache/freemarker/core/util/FTLUtil.java
+++ b/freemarker-core/src/main/java/org/apache/freemarker/core/util/FTLUtil.java
@@ -49,6 +49,7 @@ import org.apache.freemarker.core.model.impl.BeanModel;
* Static utility methods that perform tasks specific to the FreeMarker Template Language (FTL).
* This is meant to be used from outside FreeMarker (i.e., it's an official, published API), not just from inside it.
*/
+// TODO [FM3] This name won't be good if the template language isn't called "FTL"...
public final class FTLUtil {
private static final char[] ESCAPES = createEscapes();