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 <ddek...@apache.org> Authored: Mon Jul 24 20:26:29 2017 +0200 Committer: ddekany <ddek...@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 <@foo></@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>${name}</tt>{@code</@foo>}, + * {@code <@foo>Name: <#if showIt>Joe</#if></@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();