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 2018/02/28 19:49:54 UTC

incubator-freemarker git commit: FREEMARKER-84: Added .get_optional_template

Repository: incubator-freemarker
Updated Branches:
  refs/heads/2.3-gae 59f2e7b8c -> 51c247662


FREEMARKER-84: Added .get_optional_template


Project: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/commit/51c24766
Tree: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/tree/51c24766
Diff: http://git-wip-us.apache.org/repos/asf/incubator-freemarker/diff/51c24766

Branch: refs/heads/2.3-gae
Commit: 51c2476621809d8f4183f23e894be0106cabe810
Parents: 59f2e7b
Author: ddekany <dd...@apache.org>
Authored: Wed Feb 28 20:49:36 2018 +0100
Committer: ddekany <dd...@apache.org>
Committed: Wed Feb 28 20:49:36 2018 +0100

----------------------------------------------------------------------
 .../java/freemarker/core/BuiltinVariable.java   |  11 +-
 .../core/GetOptionalTemplateMethod.java         | 202 +++++++++++++++++++
 src/manual/en_US/book.xml                       | 146 +++++++++++++-
 .../core/GetOptionalTemplateTest.java           | 179 ++++++++++++++++
 .../template/utility/TemplateModelUtilTest.java |  35 ++--
 5 files changed, 556 insertions(+), 17 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/51c24766/src/main/java/freemarker/core/BuiltinVariable.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/BuiltinVariable.java b/src/main/java/freemarker/core/BuiltinVariable.java
index 41286dd..1b7617c 100644
--- a/src/main/java/freemarker/core/BuiltinVariable.java
+++ b/src/main/java/freemarker/core/BuiltinVariable.java
@@ -72,7 +72,8 @@ final class BuiltinVariable extends Expression {
     static final String URL_ESCAPING_CHARSET_CC = "urlEscapingCharset";
     static final String URL_ESCAPING_CHARSET = "url_escaping_charset";
     static final String NOW = "now";
-    
+    static final String GET_OPTIONAL_TEMPLATE = "get_optional_template";
+    static final String GET_OPTIONAL_TEMPLATE_CC = "getOptionalTemplate";
     static final String[] SPEC_VAR_NAMES = new String[] {
         AUTO_ESC_CC,
         AUTO_ESC,
@@ -83,6 +84,8 @@ final class BuiltinVariable extends Expression {
         DATA_MODEL_CC,
         DATA_MODEL,
         ERROR,
+        GET_OPTIONAL_TEMPLATE_CC,
+        GET_OPTIONAL_TEMPLATE,
         GLOBALS,
         INCOMPATIBLE_IMPROVEMENTS_CC,
         INCOMPATIBLE_IMPROVEMENTS,
@@ -239,6 +242,12 @@ final class BuiltinVariable extends Expression {
         if (name == INCOMPATIBLE_IMPROVEMENTS || name == INCOMPATIBLE_IMPROVEMENTS_CC) {
             return new SimpleScalar(env.getConfiguration().getIncompatibleImprovements().toString());
         }
+        if (name == GET_OPTIONAL_TEMPLATE) {
+            return GetOptionalTemplateMethod.INSTANCE;
+        }
+        if (name == GET_OPTIONAL_TEMPLATE_CC) {
+            return GetOptionalTemplateMethod.INSTANCE_CC;
+        }
         
         throw new _MiscTemplateException(this,
                 "Invalid special variable: ", name);

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/51c24766/src/main/java/freemarker/core/GetOptionalTemplateMethod.java
----------------------------------------------------------------------
diff --git a/src/main/java/freemarker/core/GetOptionalTemplateMethod.java b/src/main/java/freemarker/core/GetOptionalTemplateMethod.java
new file mode 100644
index 0000000..090341b
--- /dev/null
+++ b/src/main/java/freemarker/core/GetOptionalTemplateMethod.java
@@ -0,0 +1,202 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.core;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import freemarker.template.MalformedTemplateNameException;
+import freemarker.template.SimpleHash;
+import freemarker.template.Template;
+import freemarker.template.TemplateBooleanModel;
+import freemarker.template.TemplateDirectiveBody;
+import freemarker.template.TemplateDirectiveModel;
+import freemarker.template.TemplateException;
+import freemarker.template.TemplateHashModelEx;
+import freemarker.template.TemplateHashModelEx2.KeyValuePair;
+import freemarker.template.TemplateHashModelEx2.KeyValuePairIterator;
+import freemarker.template.TemplateMethodModelEx;
+import freemarker.template.TemplateModel;
+import freemarker.template.TemplateModelException;
+import freemarker.template.TemplateScalarModel;
+import freemarker.template.utility.TemplateModelUtils;
+
+/**
+ * Implements {@code .get_optional_template(name, options)}.
+ */
+class GetOptionalTemplateMethod implements TemplateMethodModelEx {
+
+    static final GetOptionalTemplateMethod INSTANCE = new GetOptionalTemplateMethod(
+            BuiltinVariable.GET_OPTIONAL_TEMPLATE);
+    static final GetOptionalTemplateMethod INSTANCE_CC = new GetOptionalTemplateMethod(
+            BuiltinVariable.GET_OPTIONAL_TEMPLATE_CC);
+    
+    private static final String OPTION_ENCODING = "encoding";
+    private static final String OPTION_PARSE = "parse";
+
+    private static final String RESULT_INCLUDE = "include";
+    private static final String RESULT_IMPORT = "import";
+    private static final String RESULT_EXISTS = "exists";
+   
+    /** Used in error messages */
+    private final String methodName;
+
+    private GetOptionalTemplateMethod(String builtInVarName) {
+        this.methodName = "." + builtInVarName;
+    }
+
+    public Object exec(List args) throws TemplateModelException {
+        final int argCnt = args.size();
+        if (argCnt < 1 || argCnt > 2) {
+            throw _MessageUtil.newArgCntError(methodName, argCnt, 1, 2);
+        }
+
+        final Environment env = Environment.getCurrentEnvironment();
+        if (env == null) {
+            throw new IllegalStateException("No freemarer.core.Environment is associated to the current thread.");
+        }
+        
+        final String absTemplateName;
+        {
+            TemplateModel arg = (TemplateModel) args.get(0);
+            if (!(arg instanceof TemplateScalarModel)) {
+                throw _MessageUtil.newMethodArgMustBeStringException(methodName, 0, arg);
+            }
+            String templateName  = EvalUtil.modelToString((TemplateScalarModel) arg, null, env);
+            
+            try {
+                absTemplateName = env.toFullTemplateName(env.getCurrentTemplate().getName(), templateName);
+            } catch (MalformedTemplateNameException e) {
+                throw new _TemplateModelException(
+                        e, "Failed to convert template path to full path; see cause exception.");
+            }
+        }
+        
+        final TemplateHashModelEx options;
+        if (argCnt > 1) {
+            TemplateModel arg = (TemplateModel) args.get(1);
+            if (!(arg instanceof TemplateHashModelEx)) {
+                throw _MessageUtil.newMethodArgMustBeExtendedHashException(methodName, 1, arg);
+            }
+            options = (TemplateHashModelEx) arg;
+        } else {
+            options = null;
+        }
+        
+        String encoding = null;
+        boolean parse = true;
+        if (options != null) {
+            final KeyValuePairIterator kvpi = TemplateModelUtils.getKeyValuePairIterator(options);
+            while (kvpi.hasNext()) {
+                final KeyValuePair kvp = kvpi.next();
+                
+                final String optName;
+                {
+                    TemplateModel optNameTM = kvp.getKey();
+                    if (!(optNameTM instanceof TemplateScalarModel)) {
+                        throw _MessageUtil.newMethodArgInvalidValueException(methodName, 1,
+                                "All keys in the options hash must be strings, but found ",
+                                new _DelayedAOrAn(new _DelayedFTLTypeDescription(optNameTM)));
+                    }
+                    optName = ((TemplateScalarModel) optNameTM).getAsString();
+                }
+                
+                final TemplateModel optValue = kvp.getValue();
+                
+                if (OPTION_ENCODING.equals(optName)) {
+                    encoding = getStringOption(OPTION_ENCODING, optValue); 
+                } else if (OPTION_PARSE.equals(optName)) {
+                    parse = getBooleanOption(OPTION_PARSE, optValue); 
+                } else {
+                    throw _MessageUtil.newMethodArgInvalidValueException(methodName, 1,
+                            "Unsupported option ", new _DelayedJQuote(optName), "; valid names are: ",
+                            new _DelayedJQuote(OPTION_ENCODING), ", ", new _DelayedJQuote(OPTION_PARSE), ".");
+                }
+            }
+        }
+
+        final Template template;
+        try {
+            template = env.getTemplateForInclusion(absTemplateName, encoding, parse, true);
+        } catch (IOException e) {
+            throw new _TemplateModelException(
+                    "Error when trying to include template ", new _DelayedJQuote(absTemplateName));
+        }
+        
+        SimpleHash result = new SimpleHash(env.getObjectWrapper());
+        result.put(RESULT_EXISTS, template != null);
+        // If the template is missing, result.include and such will be missing to, so that a default can be
+        // conveniently provided like in <@optTemp.include!myDefaultMacro />.
+        if (template != null) {
+            result.put(RESULT_INCLUDE, new TemplateDirectiveModel() {
+                public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body)
+                        throws TemplateException, IOException {
+                    if (!params.isEmpty()) {
+                        throw new TemplateException("This directive supports no parameters.", env);
+                    }
+                    if (loopVars.length != 0) {
+                        throw new TemplateException("This directive supports no loop variables.", env);
+                    }
+                    if (body != null) {
+                        throw new TemplateException("This directive supports no nested conetnt.", env);
+                    }
+                    
+                    env.include(template);
+                }
+            });
+            result.put(RESULT_IMPORT, new TemplateMethodModelEx() {
+                public Object exec(List args) throws TemplateModelException {
+                    if (!args.isEmpty()) {
+                        throw new TemplateModelException("This method supports no parameters.");
+                    }
+                    
+                    try {
+                        return env.importLib(template, null);
+                    } catch (IOException e) {
+                        throw new _TemplateModelException(e, "Failed to import loaded template; see cause exception");
+                    } catch (TemplateException e) {
+                        throw new _TemplateModelException(e, "Failed to import loaded template; see cause exception");
+                    }
+                }
+            });
+        }
+        return result;
+    }
+
+    private boolean getBooleanOption(String optionName, TemplateModel value) throws TemplateModelException {
+        if (!(value instanceof TemplateBooleanModel)) {
+            throw _MessageUtil.newMethodArgInvalidValueException(methodName, 1,
+                    "The value of the ", new _DelayedJQuote(optionName), " option must be a boolean, but it was ",
+                    new _DelayedAOrAn(new _DelayedFTLTypeDescription(value)), ".");
+        }
+        return ((TemplateBooleanModel) value).getAsBoolean();
+    }
+
+    private String getStringOption(String optionName, TemplateModel value) throws TemplateModelException {
+        if (!(value instanceof TemplateScalarModel)) {
+            throw _MessageUtil.newMethodArgInvalidValueException(methodName, 1,
+                    "The value of the ", new _DelayedJQuote(optionName), " option must be a string, but it was ",
+                    new _DelayedAOrAn(new _DelayedFTLTypeDescription(value)), ".");
+        }
+        return EvalUtil.modelToString((TemplateScalarModel) value, null, null);
+    }
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/51c24766/src/manual/en_US/book.xml
----------------------------------------------------------------------
diff --git a/src/manual/en_US/book.xml b/src/manual/en_US/book.xml
index 56851f3..7fa85fa 100644
--- a/src/manual/en_US/book.xml
+++ b/src/manual/en_US/book.xml
@@ -23378,6 +23378,14 @@ There was no specific handler for node y
         </listitem>
 
         <listitem>
+          <para><literal>get_optional_template</literal>: This is a method
+          that's used when you need to include or import a template that's
+          possibly missing, and you need to handle that case on some special
+          way. <link linkend="ref_specvar_get_optional_template">More
+          details...</link></para>
+        </listitem>
+
+        <listitem>
           <para><literal>pass</literal>: This is a macro that does nothing. It
           has no parameters. Mostly used as no-op node handler in XML
           processing.</para>
@@ -23432,7 +23440,9 @@ There was no specific handler for node y
         </listitem>
 
         <listitem>
-          <para><literal>vars</literal>: Expression
+          <para><indexterm>
+              <primary>vars</primary>
+            </indexterm><literal>vars</literal>: Expression
           <literal>.vars.foo</literal> returns the same variable as expression
           <literal>foo</literal>. It's useful if for some reasons you have to
           use square bracket syntax, since that works only for hash sub
@@ -23459,6 +23469,140 @@ There was no specific handler for node y
           2.3.21-nightly_20140726T151800Z.</para>
         </listitem>
       </itemizedlist>
+
+      <simplesect xml:id="ref_specvar_get_optional_template">
+        <title>Using get_optional_template</title>
+
+        <indexterm>
+          <primary>get_optional_template</primary>
+        </indexterm>
+
+        <indexterm>
+          <primary>include optional</primary>
+        </indexterm>
+
+        <indexterm>
+          <primary>import optional</primary>
+        </indexterm>
+
+        <para>This special variable is used when you need to include or import
+        a template that's possibly missing, and you need to handle that case
+        on some special way. It a method (so you meant to call it) that has
+        the following parameters:</para>
+
+        <orderedlist>
+          <listitem>
+            <para>The name of the template (can be relative or absolute), like
+            <literal>"/commonds/footer.ftl"</literal>; similar to the first
+            parameter of the <link
+            linkend="ref.directive.include"><literal>include</literal>
+            directive</link>. Required, string.</para>
+          </listitem>
+
+          <listitem>
+            <para>An optional hash of options, like <literal>{ 'parse': false,
+            'encoding': 'UTF-16BE' }</literal>. The valid keys are
+            <literal>encoding</literal> and <literal>parse</literal>. The
+            meaning of these are the same as of the similarly named <link
+            linkend="ref.directive.include"><literal>include</literal>
+            directive</link> parameters.</para>
+          </listitem>
+        </orderedlist>
+
+        <para>This method returns a hash that contains the following
+        entries:</para>
+
+        <itemizedlist>
+          <listitem>
+            <para><literal>exists</literal>: A boolean that tells if the
+            template was found.</para>
+          </listitem>
+
+          <listitem>
+            <para><literal>include</literal>: A directive that, when called,
+            includes the template. Calling this directive is similar to
+            calling the <link
+            linkend="ref.directive.include"><literal>include</literal>
+            directive</link>, but of course with this you spare looking up the
+            template again. This directive has no parameters, nor nested
+            content. If <literal>exists</literal> is <literal>false</literal>,
+            this key will be missing; see later how can this be utilized with
+            <link linkend="dgui_template_exp_missing_default">the
+            <literal><replaceable>exp</replaceable>!<replaceable>default</replaceable></literal>
+            operator</link>.</para>
+          </listitem>
+
+          <listitem>
+            <para><literal>import</literal>: A method that, when called,
+            imports the template, and returns the namespace of the imported
+            template. Calling this method is similar to calling the
+            <literal>import</literal> directive, but of course with this you
+            spare looking up the template again, also, it doesn't assign the
+            namespace to anything, just returns it. The method has no
+            parameters. If <literal>exists</literal> is
+            <literal>false</literal>, this key will be missing; see later how
+            can this be utilized with <link
+            linkend="dgui_template_exp_missing_default">the
+            <literal><replaceable>exp</replaceable>!<replaceable>default</replaceable></literal>
+            operator</link>.</para>
+          </listitem>
+        </itemizedlist>
+
+        <para>When this method is called (like
+        <literal>.get_optional_template('some.ftl')</literal>), it immediately
+        loads the template if it exists, but doesn't yet process it, so it
+        doesn't have any visible effect yet. The template will be processed
+        only when <literal>include</literal> or <literal>import</literal>
+        members of the returned structure is called. (Of course, when we say
+        that it loads the template, it actually looks it up in the template
+        cache, just like the <link
+        linkend="ref.directive.include"><literal>include</literal>
+        directive</link> does.)</para>
+
+        <para>While it's not an error if the template is missing, it's an
+        error if it does exist but still can't be loaded due to syntactical
+        errors in it, or due to some I/O error.</para>
+
+        <para>Example, in which depending on if <literal>some.ftl</literal>
+        exists, we either print <quote>Template was found:</quote> and the
+        include the template as <literal>&lt;#include 'some.ftl'&gt;</literal>
+        would, otherwise it we print <quote>Template was
+        missing.</quote>:</para>
+
+        <programlisting role="template">&lt;#assign optTemp = .get_optional_template('some.ftl')&gt;
+&lt;#if optTemp.exists&gt;
+  Template was found:
+  &lt;@optTemp.include /&gt;
+&lt;#else&gt;
+  Template was missing.
+&lt;/#if&gt;</programlisting>
+
+        <para>Example, in which we try to include <literal>some.ftl</literal>,
+        but if that's missing then we try to include
+        <literal>some-fallback.ftl</literal>, and if that's missing too then
+        we call the <literal>ultimateFallback</literal> macro instead of
+        including anything (note the <literal>!</literal>-s after the
+        <literal>include</literal>-s; they belong to <link
+        linkend="dgui_template_exp_missing_default">the
+        <literal><replaceable>exp</replaceable>!<replaceable>default</replaceable></literal>
+        operator</link>):</para>
+
+        <programlisting role="template">&lt;#macro ultimateFallback&gt;
+  Something
+&lt;/#macro&gt;
+
+&lt;@(
+  .get_optional_template('some.ftl').include!
+  .get_optional_template('some-fallback.ftl').include!
+  ultimateFallback
+) /&gt;</programlisting>
+
+        <para>Example, which behaves like <literal>&lt;#import 'tags.ftl' as
+        tags&gt;</literal>, except that if <literal>tags.ftl</literal> is
+        missing, then it creates an empty <literal>tags</literal> hash:</para>
+
+        <programlisting role="template">&lt;#assign tags = (.get_optional_template('tags.ftl').import())!{}&gt;</programlisting>
+      </simplesect>
     </chapter>
 
     <chapter xml:id="ref_reservednames">

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/51c24766/src/test/java/freemarker/core/GetOptionalTemplateTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/freemarker/core/GetOptionalTemplateTest.java b/src/test/java/freemarker/core/GetOptionalTemplateTest.java
new file mode 100644
index 0000000..4ceb3e4
--- /dev/null
+++ b/src/test/java/freemarker/core/GetOptionalTemplateTest.java
@@ -0,0 +1,179 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package freemarker.core;
+
+import java.nio.charset.StandardCharsets;
+
+import org.junit.Test;
+
+import freemarker.cache.ByteArrayTemplateLoader;
+import freemarker.cache.MultiTemplateLoader;
+import freemarker.cache.StringTemplateLoader;
+import freemarker.cache.TemplateLoader;
+import freemarker.template.Configuration;
+import freemarker.test.TemplateTest;
+
+public class GetOptionalTemplateTest extends TemplateTest {
+
+    private ByteArrayTemplateLoader byteArrayTemplateLoader = new ByteArrayTemplateLoader();
+    
+    @Override
+    protected Configuration createConfiguration() throws Exception {
+        Configuration cfg = super.createConfiguration();
+        cfg.setTemplateLoader(
+                new MultiTemplateLoader(new TemplateLoader[] {
+                        new StringTemplateLoader(), byteArrayTemplateLoader
+                }));
+        return cfg;
+    }
+    
+    @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 testOptions() throws Exception {
+        addTemplate("inc.ftl", "${1}");
+        assertOutput(""
+                + "<#assign t = .getOptionalTemplate('inc.ftl', { 'parse': false })>"
+                + "<@t.include />",
+                "${1}");
+        assertOutput(""
+                + "<#assign t = .getOptionalTemplate('inc.ftl')>"
+                + "<@t.include />",
+                "1");
+        assertOutput(""
+                + "<#assign t = .getOptionalTemplate('inc.ftl', {})>"
+                + "<@t.include />",
+                "1");
+        
+        byteArrayTemplateLoader.putTemplate("inc-u16.ftl", "foo".getBytes(StandardCharsets.UTF_16BE));
+        assertOutput(""
+                + "<#assign t = .getOptionalTemplate('inc-u16.ftl', { 'encoding': 'utf-16be' })>"
+                + "<@t.include />",
+                "foo");
+        assertOutput(""
+                + "<#assign t = .getOptionalTemplate('inc-u16.ftl')>"
+                + "<@t.include />",
+                "\u0000f\u0000o\u0000o");
+        
+        byteArrayTemplateLoader.putTemplate("inc-u16.ftl", "foo${1}".getBytes(StandardCharsets.UTF_16BE));
+        assertOutput(""
+                + "<#assign t = .getOptionalTemplate('inc-u16.ftl', { 'parse': false, 'encoding': 'utf-16be' })>"
+                + "<@t.include />",
+                "foo${1}");
+    }
+
+    @Test
+    public void testRelativeAndAbsolutePath() throws Exception {
+        addTemplate("lib/inc.ftl", "included");
+        
+        addTemplate("test1.ftl", "<#include 'lib/inc.ftl'>");
+        assertOutputForNamed("test1.ftl", "included");
+        
+        addTemplate("lib/test2.ftl", "<#include '/lib/inc.ftl'>");
+        assertOutputForNamed("lib/test2.ftl", "included");
+        
+        addTemplate("lib/test3.ftl", "<#include 'inc.ftl'>");
+        assertOutputForNamed("lib/test3.ftl", "included");
+        
+        addTemplate("sub/test4.ftl", "<#include '../lib/inc.ftl'>");
+        assertOutputForNamed("sub/test4.ftl", "included");
+    }
+
+    @Test
+    public void testUseCase1() throws Exception {
+        addTemplate("lib/inc.ftl", "included");
+        assertOutput(""
+                + "<#macro test templateName>"
+                + "<#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()>", ".getOptionalTemplate", "arguments", "none");
+        assertErrorContains("<#assign t = .get_optional_template()>", ".get_optional_template", "arguments", "none");
+        assertErrorContains("<#assign t = .getOptionalTemplate(1, 2, 3)>", "arguments", "3");
+        assertErrorContains("<#assign t = .getOptionalTemplate(1)>", "#1", "string", "number");
+        assertErrorContains("<#assign t = .getOptionalTemplate('x', 1)>", "#2", "hash", "number");
+        assertErrorContains("<#assign t = .getOptionalTemplate('x', { 'foo': 1 })>",
+                "#2", "foo", "encoding", "parse");
+        assertErrorContains("<#assign t = .getOptionalTemplate('x', { 'parse': 1 })>",
+                "#2", "parse", "number", "boolean");
+        assertErrorContains("<#assign t = .getOptionalTemplate('x', { 'encoding': 1 })>",
+                "#2", "encoding", "number", "string");
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/51c24766/src/test/java/freemarker/template/utility/TemplateModelUtilTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/freemarker/template/utility/TemplateModelUtilTest.java b/src/test/java/freemarker/template/utility/TemplateModelUtilTest.java
index 6179e17..c93a2e5 100644
--- a/src/test/java/freemarker/template/utility/TemplateModelUtilTest.java
+++ b/src/test/java/freemarker/template/utility/TemplateModelUtilTest.java
@@ -33,35 +33,35 @@ import freemarker.template.DefaultNonListCollectionAdapter;
 import freemarker.template.DefaultObjectWrapperBuilder;
 import freemarker.template.TemplateCollectionModel;
 import freemarker.template.TemplateHashModelEx;
+import freemarker.template.TemplateHashModelEx2;
 import freemarker.template.TemplateHashModelEx2.KeyValuePair;
 import freemarker.template.TemplateHashModelEx2.KeyValuePairIterator;
 import freemarker.template.TemplateModel;
 import freemarker.template.TemplateModelException;
 import freemarker.template.TemplateNumberModel;
 import freemarker.template.TemplateScalarModel;
-import freemarker.test.TemplateTest;
 
-public class TemplateModelUtilTest extends TemplateTest {
+public class TemplateModelUtilTest {
 
     @Test
     public void testGetKeyValuePairIterator() throws Exception {
         Map<Object, Object> map = new LinkedHashMap<Object, Object>();
         TemplateHashModelEx thme = new TemplateHashModelExOnly(map);
         
-        assertetKeyValuePairIteratorResult("", thme);
+        assertetGetKeyValuePairIteratorContent("", thme);
         
         map.put("k1", 11);
-        assertetKeyValuePairIteratorResult("str(k1): num(11)", thme);
+        assertetGetKeyValuePairIteratorContent("str(k1): num(11)", thme);
         
         map.put("k2", "v2");
-        assertetKeyValuePairIteratorResult("str(k1): num(11), str(k2): str(v2)", thme);
+        assertetGetKeyValuePairIteratorContent("str(k1): num(11), str(k2): str(v2)", thme);
 
         map.put("k2", null);
-        assertetKeyValuePairIteratorResult("str(k1): num(11), str(k2): null", thme);
+        assertetGetKeyValuePairIteratorContent("str(k1): num(11), str(k2): null", thme);
         
         map.put(3, 33);
         try {
-            assertetKeyValuePairIteratorResult("fails anyway...", thme);
+            assertetGetKeyValuePairIteratorContent("fails anyway...", thme);
             fail();
         } catch (TemplateModelException e) {
             assertThat(e.getMessage(),
@@ -71,7 +71,7 @@ public class TemplateModelUtilTest extends TemplateTest {
         
         map.put(null, 44);
         try {
-            assertetKeyValuePairIteratorResult("fails anyway...", thme);
+            assertetGetKeyValuePairIteratorContent("fails anyway...", thme);
             fail();
         } catch (TemplateModelException e) {
             assertThat(e.getMessage(),
@@ -83,19 +83,20 @@ public class TemplateModelUtilTest extends TemplateTest {
     public void testGetKeyValuePairIteratorWithEx2() throws Exception {
         Map<Object, Object> map = new LinkedHashMap<Object, Object>();
         TemplateHashModelEx thme = DefaultMapAdapter.adapt(
-                map, (ObjectWrapperWithAPISupport) getConfiguration().getObjectWrapper());
+                map, new DefaultObjectWrapperBuilder(Configuration.VERSION_2_3_27).build());
         
-        assertetKeyValuePairIteratorResult("", thme);
+        assertetGetKeyValuePairIteratorContent("", thme);
         
         map.put("k1", 11);
         map.put("k2", "v2");
         map.put("k2", null);
         map.put(3, 33);
         map.put(null, 44);
-        assertetKeyValuePairIteratorResult("str(k1): num(11), str(k2): null, num(3): num(33), null: num(44)", thme);
+        assertetGetKeyValuePairIteratorContent(
+                "str(k1): num(11), str(k2): null, num(3): num(33), null: num(44)", thme);
     }
     
-    private void assertetKeyValuePairIteratorResult(String expected, TemplateHashModelEx thme)
+    private void assertetGetKeyValuePairIteratorContent(String expected, TemplateHashModelEx thme)
             throws TemplateModelException {
          StringBuilder sb = new StringBuilder();
          KeyValuePairIterator kvpi = TemplateModelUtils.getKeyValuePairIterator(thme);
@@ -104,11 +105,12 @@ public class TemplateModelUtilTest extends TemplateTest {
              if (sb.length() != 0) {
                  sb.append(", ");
              }
-             sb.append(toAssertionString(kvp.getKey())).append(": ").append(toAssertionString(kvp.getValue()));
+             sb.append(toValueAssertionString(kvp.getKey())).append(": ")
+                     .append(toValueAssertionString(kvp.getValue()));
          }
     }
     
-    private String toAssertionString(TemplateModel model) throws TemplateModelException {
+    private String toValueAssertionString(TemplateModel model) throws TemplateModelException {
         if (model instanceof TemplateNumberModel) {
             return "num(" + ((TemplateNumberModel) model).getAsNumber() + ")";
         } else if (model instanceof TemplateScalarModel) {
@@ -120,6 +122,9 @@ public class TemplateModelUtilTest extends TemplateTest {
         throw new IllegalArgumentException("Type unsupported by test: " + model.getClass().getName());
     }
 
+    /**
+     * Deliberately doesn't implement {@link TemplateHashModelEx2}, only {@link TemplateHashModelEx}. 
+     */
     private static class TemplateHashModelExOnly implements TemplateHashModelEx {
         
         private final Map<?, ?> map;
@@ -139,7 +144,7 @@ public class TemplateModelUtilTest extends TemplateTest {
         }
 
         public int size() throws TemplateModelException {
-            return 2;
+            return map.size();
         }
 
         public TemplateCollectionModel keys() throws TemplateModelException {