Forward ported .getOptionalTemplate from 2.3-gae. Note that we don't have "encoding" and "parse" options, as they aren't relevant in FM3.
Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/99174103 Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/99174103 Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/99174103 Branch: refs/heads/3 Commit: 99174103a9a53e7d596deb3fa00aac2420aceefb Parents: f9fa582 Author: ddekany <ddek...@apache.org> Authored: Fri Mar 2 13:11:30 2018 +0100 Committer: ddekany <ddek...@apache.org> Committed: Fri Mar 2 13:11:30 2018 +0100 ---------------------------------------------------------------------- .../core/GetOptionalTemplateTest.java | 114 ++++++++++++++++++ .../freemarker/core/ASTExpBuiltInVariable.java | 5 + .../core/GetOptionalTemplateFunction.java | 117 +++++++++++++++++++ 3 files changed, 236 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/99174103/freemarker-core-test/src/test/java/org/apache/freemarker/core/GetOptionalTemplateTest.java ---------------------------------------------------------------------- diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/GetOptionalTemplateTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/GetOptionalTemplateTest.java new file mode 100644 index 0000000..4149e02 --- /dev/null +++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/GetOptionalTemplateTest.java @@ -0,0 +1,114 @@ +package org.apache.freemarker.core; + +import org.apache.freemarker.core.templateresolver.TemplateLoader; +import org.apache.freemarker.core.templateresolver.impl.ByteArrayTemplateLoader; +import org.apache.freemarker.core.templateresolver.impl.MultiTemplateLoader; +import org.apache.freemarker.core.templateresolver.impl.StringTemplateLoader; +import org.apache.freemarker.test.TemplateTest; +import org.junit.Test; + +public class GetOptionalTemplateTest extends TemplateTest { + + private ByteArrayTemplateLoader byteArrayTemplateLoader = new ByteArrayTemplateLoader(); + + @Override + protected Configuration createDefaultConfiguration() throws Exception { + return new Configuration.Builder(Configuration.VERSION_3_0_0) + .templateLoader( + new MultiTemplateLoader(new TemplateLoader[] { + new StringTemplateLoader(), byteArrayTemplateLoader + })).build(); + } + + @Test + public void testBasicsWhenTemplateExists() throws Exception { + addTemplate("inc.ftl", "<#assign x = (x!0) + 1>inc ${x}"); + assertOutput("" + + "<#assign t = .getOptionalTemplate('inc.ftl')>" + + "Exists: ${t.exists?c}; " + + "Include: <@t.include />, <@t.include />; " + + "Import: <#assign ns1 = t.import()><#assign ns2 = t.import()>${ns1.x}, ${ns2.x}; " + + "Aliased: <#assign x = 9 in ns1>${ns1.x}, ${ns2.x}, <#import 'inc.ftl' as ns3>${ns3.x}", + "Exists: true; " + + "Include: inc 1, inc 2; " + + "Import: 1, 1; " + + "Aliased: 9, 9, 9" + ); + } + + @Test + public void testBasicsWhenTemplateIsMissing() throws Exception { + assertOutput("" + + "<#assign t = .getOptionalTemplate('missing.ftl')>" + + "Exists: ${t.exists?c}; " + + "Include: ${t.include???c}; " + + "Import: ${t.import???c}", + "Exists: false; " + + "Include: false; " + + "Import: false" + ); + } + + @Test + public void testRelativeAndAbsolutePath() throws Exception { + addTemplate("lib/inc.ftl", "included"); + + addTemplate("test1.ftl", "<@.getOptionalTemplate('lib/inc.ftl').include />"); + assertOutputForNamed("test1.ftl", "included"); + + addTemplate("lib/test2.ftl", "<@.getOptionalTemplate('/lib/inc.ftl').include />"); + assertOutputForNamed("lib/test2.ftl", "included"); + + addTemplate("lib/test3.ftl", "<@.getOptionalTemplate('inc.ftl').include />"); + assertOutputForNamed("lib/test3.ftl", "included"); + + addTemplate("sub/test4.ftl", "<@.getOptionalTemplate('../lib/inc.ftl').include />"); + assertOutputForNamed("sub/test4.ftl", "included"); + } + + @Test + public void testUseCase1() throws Exception { + addTemplate("lib/inc.ftl", "included"); + assertOutput("" + + "<#macro test templateName{positional}>" + + "<#local t = .getOptionalTemplate(templateName)>" + + "<#if t.exists>" + + "before <@t.include /> after" + + "<#else>" + + "missing" + + "</#if>" + + "</#macro>" + + "<@test 'lib/inc.ftl' />; " + + "<@test 'inc.ftl' />", + "before included after; missing"); + } + + @Test + public void testUseCase2() throws Exception { + addTemplate("found.ftl", "found"); + assertOutput("" + + "<@(" + + ".getOptionalTemplate('missing1.ftl').include!" + + ".getOptionalTemplate('missing2.ftl').include!" + + ".getOptionalTemplate('found.ftl').include!" + + ".getOptionalTemplate('missing3.ftl').include" + + ") />", + "found"); + assertOutput("" + + "<#macro fallback>fallback</#macro>" + + "<@(" + + ".getOptionalTemplate('missing1.ftl').include!" + + ".getOptionalTemplate('missing2.ftl').include!" + + "fallback" + + ") />", + "fallback"); + } + + @Test + public void testWrongArguments() throws Exception { + assertErrorContains("<#assign t = .getOptionalTemplate()>", "argument"); + assertErrorContains("<#assign t = .getOptionalTemplate('1', '2', '3')>", "arguments", "3"); + assertErrorContains("<#assign t = .getOptionalTemplate(1)>", "1st argument", "string", "number"); + } + +} http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/99174103/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltInVariable.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltInVariable.java b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltInVariable.java index 19b0b04..b9d2f12 100644 --- a/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltInVariable.java +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/ASTExpBuiltInVariable.java @@ -57,6 +57,7 @@ final class ASTExpBuiltInVariable extends ASTExpression { static final String AUTO_ESC = "autoEsc"; static final String URL_ESCAPING_CHARSET = "urlEscapingCharset"; static final String NOW = "now"; + static final String GET_OPTIONAL_TEMPLATE = "getOptionalTemplate"; static final Set<String> BUILT_IN_VARIABLE_NAMES = new _SortedArraySet<>( // Must be sorted alphabetically! @@ -64,6 +65,7 @@ final class ASTExpBuiltInVariable extends ASTExpression { CURRENT_TEMPLATE_NAME, DATA_MODEL, ERROR, + GET_OPTIONAL_TEMPLATE, GLOBALS, INCOMPATIBLE_IMPROVEMENTS, LANG, @@ -194,6 +196,9 @@ final class ASTExpBuiltInVariable extends ASTExpression { if (name == INCOMPATIBLE_IMPROVEMENTS) { return new SimpleString(env.getConfiguration().getIncompatibleImprovements().toString()); } + if (name == GET_OPTIONAL_TEMPLATE) { + return GetOptionalTemplateFunction.INSTANCE; + } throw new TemplateException(this, "Invalid special variable: ", name); http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/99174103/freemarker-core/src/main/java/org/apache/freemarker/core/GetOptionalTemplateFunction.java ---------------------------------------------------------------------- diff --git a/freemarker-core/src/main/java/org/apache/freemarker/core/GetOptionalTemplateFunction.java b/freemarker-core/src/main/java/org/apache/freemarker/core/GetOptionalTemplateFunction.java new file mode 100644 index 0000000..eb8b6da --- /dev/null +++ b/freemarker-core/src/main/java/org/apache/freemarker/core/GetOptionalTemplateFunction.java @@ -0,0 +1,117 @@ +/* + * 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 static org.apache.freemarker.core.util.CallableUtils.*; + +import java.io.IOException; +import java.io.Writer; + +import org.apache.freemarker.core.model.ArgumentArrayLayout; +import org.apache.freemarker.core.model.TemplateBooleanModel; +import org.apache.freemarker.core.model.TemplateDirectiveModel; +import org.apache.freemarker.core.model.TemplateFunctionModel; +import org.apache.freemarker.core.model.TemplateModel; +import org.apache.freemarker.core.templateresolver.MalformedTemplateNameException; + +/** + * Implements {@code .getOptionalTemplate(name, options)}. + */ +class GetOptionalTemplateFunction implements TemplateFunctionModel { + + static final GetOptionalTemplateFunction INSTANCE = new GetOptionalTemplateFunction(); + + private static final String RESULT_INCLUDE = "include"; + private static final String RESULT_IMPORT = "import"; + private static final String RESULT_EXISTS = "exists"; + + private GetOptionalTemplateFunction() { + // No op. + } + + @Override + public TemplateModel execute(TemplateModel[] args, CallPlace callPlace, Environment env) throws TemplateException { + final String absTemplateName; + try { + absTemplateName = env.toFullTemplateName( + env.getCurrentTemplate().getLookupName(), + getStringArgument(args, 0, this)); + } catch (MalformedTemplateNameException e) { + throw new TemplateException("Failed to convert template path to full path; see cause exception.", e); + } + + final Template template; + try { + template = env.getTemplateForInclusion(absTemplateName, true); + } catch (IOException e) { + throw new TemplateException( + "Error when trying to include template ", new _DelayedJQuote(absTemplateName)); + } + + NativeHashEx result = new NativeHashEx(); + result.put(RESULT_EXISTS, template != null ? TemplateBooleanModel.TRUE : TemplateBooleanModel.FALSE); + // If the template is missing, result.include and such will be missing too, so that a default can be + // conveniently provided like in <@optTemp.include!myDefaultMacro />. + if (template != null) { + result.put(RESULT_INCLUDE, new TemplateDirectiveModel() { + @Override + public void execute(TemplateModel[] args, CallPlace callPlace, Writer out, Environment env) + throws TemplateException, IOException { + env.include(template); + } + + @Override + public boolean isNestedContentSupported() { + return false; + } + + @Override + public ArgumentArrayLayout getDirectiveArgumentArrayLayout() { + return ArgumentArrayLayout.PARAMETERLESS; + } + }); + result.put(RESULT_IMPORT, new TemplateFunctionModel() { + @Override + public TemplateModel execute(TemplateModel[] args, CallPlace callPlace, Environment env) + throws TemplateException { + try { + return env.importLib(template, null); + } catch (IOException e) { + throw new TemplateException(e, "Failed to import loaded template; see cause exception"); + } catch (TemplateException e) { + throw new TemplateException(e, "Failed to import loaded template; see cause exception"); + } + } + + @Override + public ArgumentArrayLayout getFunctionArgumentArrayLayout() { + return ArgumentArrayLayout.PARAMETERLESS; + } + }); + } + return result; + } + + @Override + public ArgumentArrayLayout getFunctionArgumentArrayLayout() { + return ArgumentArrayLayout.SINGLE_POSITIONAL_PARAMETER; + } + +}