You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@freemarker.apache.org by dd...@apache.org on 2017/05/15 21:24:00 UTC

[44/51] [abbrv] [partial] incubator-freemarker git commit: Restructured project so that freemarker-test-utils depends on freemarker-core (and hence can provide common classes for testing templates, and can use utility classes defined in the core). As a c

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/OutputFormatTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/OutputFormatTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/OutputFormatTest.java
new file mode 100644
index 0000000..eedb4d1
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/OutputFormatTest.java
@@ -0,0 +1,1068 @@
+/*
+ * 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.ParsingConfiguration.*;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.Collections;
+
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.outputformat.OutputFormat;
+import org.apache.freemarker.core.outputformat.impl.HTMLOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.PlainTextOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.RTFOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.UndefinedOutputFormat;
+import org.apache.freemarker.core.outputformat.impl.XMLOutputFormat;
+import org.apache.freemarker.core.templateresolver.ConditionalTemplateConfigurationFactory;
+import org.apache.freemarker.core.templateresolver.FileNameGlobMatcher;
+import org.apache.freemarker.core.templateresolver.OrMatcher;
+import org.apache.freemarker.core.templateresolver.impl.NullCacheStorage;
+import org.apache.freemarker.core.userpkg.CustomHTMLOutputFormat;
+import org.apache.freemarker.core.userpkg.DummyOutputFormat;
+import org.apache.freemarker.core.userpkg.SeldomEscapedOutputFormat;
+import org.apache.freemarker.test.TemplateTest;
+import org.apache.freemarker.test.TestConfigurationBuilder;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+
+public class OutputFormatTest extends TemplateTest {
+
+    @Test
+    public void testOutputFormatSettingLayers() throws Exception {
+        addTemplate("t", "${.outputFormat}");
+        addTemplate("t.xml", "${.outputFormat}");
+        addTemplate("tWithHeader", "<#ftl outputFormat='HTML'>${.outputFormat}");
+        
+        TestConfigurationBuilder cfgB = createDefaultConfigurationBuilder();
+        for (OutputFormat cfgOutputFormat
+                : new OutputFormat[] { UndefinedOutputFormat.INSTANCE, RTFOutputFormat.INSTANCE } ) {
+            if (!cfgOutputFormat.equals(UndefinedOutputFormat.INSTANCE)) {
+                cfgB.setOutputFormat(cfgOutputFormat);
+            }
+            setConfiguration(cfgB.build());
+
+            assertEquals(cfgOutputFormat, getConfiguration().getOutputFormat());
+            
+            {
+                Template t = getConfiguration().getTemplate("t");
+                assertEquals(cfgOutputFormat, t.getOutputFormat());
+                assertOutput(t, t.getOutputFormat().getName());
+            }
+            
+            {
+                Template t = getConfiguration().getTemplate("t.xml");
+                assertEquals(XMLOutputFormat.INSTANCE, t.getOutputFormat());
+                assertOutput(t, t.getOutputFormat().getName());
+            }
+            
+            {
+                Template t = getConfiguration().getTemplate("tWithHeader");
+                assertEquals(HTMLOutputFormat.INSTANCE, t.getOutputFormat());
+                assertOutput(t, t.getOutputFormat().getName());
+            }
+            
+            getConfiguration().clearTemplateCache();
+        }
+    }
+    
+    @Test
+    public void testStandardFileExtensions() throws Exception {
+        String commonContent = "${.outputFormat}";
+        addTemplate("t", commonContent);
+        addTemplate("t.ftl", commonContent);
+        addTemplate("t.ftlh", commonContent);
+        addTemplate("t.FTLH", commonContent);
+        addTemplate("t.fTlH", commonContent);
+        addTemplate("t.ftlx", commonContent);
+        addTemplate("t.FTLX", commonContent);
+        addTemplate("t.fTlX", commonContent);
+        addTemplate("tWithHeader.ftlx", "<#ftl outputFormat='HTML'>" + commonContent);
+        
+        TestConfigurationBuilder cfgB = createDefaultConfigurationBuilder();
+        for (int setupNumber = 1; setupNumber <= 3; setupNumber++) {
+            final OutputFormat cfgOutputFormat;
+            final OutputFormat ftlhOutputFormat;
+            final OutputFormat ftlxOutputFormat;
+            switch (setupNumber) {
+            case 1:
+                cfgOutputFormat = UndefinedOutputFormat.INSTANCE;
+                ftlhOutputFormat = HTMLOutputFormat.INSTANCE;
+                ftlxOutputFormat = XMLOutputFormat.INSTANCE;
+                break;
+            case 2:
+                cfgOutputFormat = RTFOutputFormat.INSTANCE;
+                cfgB.setOutputFormat(cfgOutputFormat);
+                ftlhOutputFormat = HTMLOutputFormat.INSTANCE;
+                ftlxOutputFormat = XMLOutputFormat.INSTANCE;
+                break;
+            case 3:
+                cfgOutputFormat = UndefinedOutputFormat.INSTANCE;
+                cfgB.unsetOutputFormat();
+                TemplateConfiguration.Builder tcbXML = new TemplateConfiguration.Builder();
+                tcbXML.setOutputFormat(XMLOutputFormat.INSTANCE);
+                cfgB.setTemplateConfigurations(
+                        new ConditionalTemplateConfigurationFactory(
+                                new OrMatcher(
+                                        new FileNameGlobMatcher("*.ftlh"),
+                                        new FileNameGlobMatcher("*.FTLH"),
+                                        new FileNameGlobMatcher("*.fTlH")),
+                                tcbXML.build()));
+                ftlhOutputFormat = HTMLOutputFormat.INSTANCE; // can't be overidden
+                ftlxOutputFormat = XMLOutputFormat.INSTANCE;
+                break;
+            default:
+                throw new AssertionError();
+            }
+
+            setConfiguration(cfgB.build());
+            assertEquals(cfgOutputFormat, getConfiguration().getOutputFormat());
+            
+            {
+                Template t = getConfiguration().getTemplate("t");
+                assertEquals(cfgOutputFormat, t.getOutputFormat());
+                assertOutput(t, t.getOutputFormat().getName());
+            }
+            
+            {
+                Template t = getConfiguration().getTemplate("t.ftl");
+                assertEquals(cfgOutputFormat, t.getOutputFormat());
+                assertOutput(t, t.getOutputFormat().getName());
+            }
+            
+            for (String name : new String[] { "t.ftlh", "t.FTLH", "t.fTlH" }) {
+                Template t = getConfiguration().getTemplate(name);
+                assertEquals(ftlhOutputFormat, t.getOutputFormat());
+                assertOutput(t, t.getOutputFormat().getName());
+            }
+            
+            for (String name : new String[] { "t.ftlx", "t.FTLX", "t.fTlX" }) {
+                Template t = getConfiguration().getTemplate(name);
+                assertEquals(ftlxOutputFormat, t.getOutputFormat());
+                assertOutput(t, t.getOutputFormat().getName());
+            }
+
+            {
+                Template t = getConfiguration().getTemplate("tWithHeader.ftlx");
+                assertEquals(HTMLOutputFormat.INSTANCE, t.getOutputFormat());
+                assertOutput(t, t.getOutputFormat().getName());
+            }
+
+            getConfiguration().clearTemplateCache();
+        }
+    }
+    
+    @Test
+    public void testStandardFileExtensionsSettingOverriding() throws Exception {
+        addTemplate("t.ftlx",
+                "${\"'\"} ${\"'\"?esc} ${\"'\"?noEsc}");
+        addTemplate("t.ftl",
+                "${'{}'} ${'{}'?esc} ${'{}'?noEsc}");
+        
+        ConditionalTemplateConfigurationFactory tcfHTML = new ConditionalTemplateConfigurationFactory(
+                new FileNameGlobMatcher("t.*"),
+                new TemplateConfiguration.Builder()
+                        .outputFormat(HTMLOutputFormat.INSTANCE)
+                        .build());
+
+        ConditionalTemplateConfigurationFactory tcfNoAutoEsc = new ConditionalTemplateConfigurationFactory(
+                new FileNameGlobMatcher("t.*"),
+                new TemplateConfiguration.Builder()
+                        .autoEscapingPolicy(DISABLE_AUTO_ESCAPING_POLICY)
+                        .build());
+
+        {
+            TestConfigurationBuilder cfgB = createDefaultConfigurationBuilder();
+
+            setConfiguration(cfgB.outputFormat(HTMLOutputFormat.INSTANCE).build());
+            assertOutputForNamed("t.ftlx", "&apos; &apos; '");  // Can't override it
+            setConfiguration(cfgB.templateConfigurations(tcfHTML).build());
+            assertOutputForNamed("t.ftlx", "&apos; &apos; '");  // Can't override it
+            setConfiguration(cfgB.templateConfigurations(tcfNoAutoEsc).build());
+            assertOutputForNamed("t.ftlx", "&apos; &apos; '");  // Can't override it
+        }
+
+        {
+            TestConfigurationBuilder cfgB = createDefaultConfigurationBuilder();
+
+            setConfiguration(cfgB.recognizeStandardFileExtensions(false).build());
+            assertErrorContainsForNamed("t.ftlx", UndefinedOutputFormat.INSTANCE.getName());
+            setConfiguration(cfgB.outputFormat(HTMLOutputFormat.INSTANCE).build());
+            assertOutputForNamed("t.ftlx", "&#39; &#39; '");
+            setConfiguration(cfgB.outputFormat(XMLOutputFormat.INSTANCE).build());
+            assertOutputForNamed("t.ftlx", "&apos; &apos; '");
+            setConfiguration(cfgB.templateConfigurations(tcfHTML).build());
+            assertOutputForNamed("t.ftlx", "&#39; &#39; '");
+            setConfiguration(cfgB.templateConfigurations(tcfNoAutoEsc).build());
+            assertOutputForNamed("t.ftlx", "' &apos; '");
+        }
+
+        {
+            TestConfigurationBuilder cfgB = createDefaultConfigurationBuilder();
+            cfgB.setRecognizeStandardFileExtensions(true);
+
+            setConfiguration(cfgB.templateConfigurations(tcfHTML).build());
+            assertOutputForNamed("t.ftlx", "&apos; &apos; '");  // Can't override it
+            setConfiguration(cfgB.templateConfigurations(tcfNoAutoEsc).build());
+            assertOutputForNamed("t.ftlx", "&apos; &apos; '");  // Can't override it
+        }
+
+        {
+            TestConfigurationBuilder cfgB = createDefaultConfigurationBuilder();
+
+            setConfiguration(cfgB.templateConfigurations(tcfHTML).build());
+            assertOutputForNamed("t.ftlx", "&apos; &apos; '");  // Can't override it
+            setConfiguration(cfgB.recognizeStandardFileExtensions(false).build());
+            assertOutputForNamed("t.ftlx", "&#39; &#39; '");
+        }
+    }
+
+    @Test
+    public void testStandardFileExtensionsWithConstructor() throws Exception {
+        Configuration cfg = getConfiguration();
+        String commonFTL = "${'\\''}";
+        {
+            Template t = new Template("foo.ftl", commonFTL, cfg);
+            assertSame(UndefinedOutputFormat.INSTANCE, t.getOutputFormat());
+            StringWriter out = new StringWriter();
+            t.process(null, out);
+            assertEquals("'", out.toString());
+        }
+        {
+            Template t = new Template("foo.ftlx", commonFTL, cfg);
+            assertSame(XMLOutputFormat.INSTANCE, t.getOutputFormat());
+            StringWriter out = new StringWriter();
+            t.process(null, out);
+            assertEquals("&apos;", out.toString());
+        }
+        {
+            Template t = new Template("foo.ftlh", commonFTL, cfg);
+            assertSame(HTMLOutputFormat.INSTANCE, t.getOutputFormat());
+            StringWriter out = new StringWriter();
+            t.process(null, out);
+            assertEquals("&#39;", out.toString());
+        }
+    }
+    
+    @Test
+    public void testStandardFileExtensionsFormatterImplOverriding() throws Exception {
+        addTemplate("t.ftlh", "${'a&x'}");
+
+        assertOutputForNamed("t.ftlh", "a&amp;x");
+
+        setConfiguration(new TestConfigurationBuilder()
+                .registeredCustomOutputFormats(Collections.<OutputFormat>singleton(CustomHTMLOutputFormat.INSTANCE))
+                .build());
+        assertOutputForNamed("t.ftlh", "a&amp;X");
+
+        setConfiguration(new TestConfigurationBuilder()
+                .registeredCustomOutputFormats(Collections.<OutputFormat>emptyList())
+                .build());
+        assertOutputForNamed("t.ftlh", "a&amp;x");
+    }
+    
+    @Test
+    public void testAutoEscapingSettingLayers() throws Exception {
+        addTemplate("t", "${'a&b'}");
+        addTemplate("tWithHeaderFalse", "<#ftl autoEsc=false>${'a&b'}");
+        addTemplate("tWithHeaderTrue", "<#ftl autoEsc=true>${'a&b'}");
+        
+        TestConfigurationBuilder cfgB = createDefaultConfigurationBuilder().outputFormat(XMLOutputFormat.INSTANCE);
+        assertEquals(ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY, cfgB.getAutoEscapingPolicy());
+
+        for (boolean cfgAutoEscaping : new boolean[] { true, false }) {
+            if (!cfgAutoEscaping) {
+                cfgB.setAutoEscapingPolicy(DISABLE_AUTO_ESCAPING_POLICY);
+            }
+            setConfiguration(cfgB.build());
+
+            {
+                Template t = getConfiguration().getTemplate("t");
+                if (cfgAutoEscaping) {
+                    assertEquals(ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY, t.getAutoEscapingPolicy());
+                    assertOutput(t, "a&amp;b");
+                } else {
+                    assertEquals(DISABLE_AUTO_ESCAPING_POLICY, t.getAutoEscapingPolicy());
+                    assertOutput(t, "a&b");
+                }
+            }
+            
+            {
+                Template t = getConfiguration().getTemplate("tWithHeaderFalse");
+                assertEquals(DISABLE_AUTO_ESCAPING_POLICY, t.getAutoEscapingPolicy());
+                assertOutput(t, "a&b");
+            }
+            
+            {
+                Template t = getConfiguration().getTemplate("tWithHeaderTrue");
+                assertEquals(ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY, t.getAutoEscapingPolicy());
+                assertOutput(t, "a&amp;b");
+            }
+
+            getConfiguration().clearTemplateCache();
+        }
+    }
+    
+    @Test
+    public void testNumericalInterpolation() throws IOException, TemplateException {
+        setConfiguration(new TestConfigurationBuilder()
+                .registeredCustomOutputFormats(Collections.<OutputFormat>singleton(DummyOutputFormat.INSTANCE))
+                .build());
+        assertOutput(
+                "<#ftl outputFormat='dummy'>#{1.5}; #{1.5; m3}; ${'a.b'}",
+                "1\\.5; 1\\.500; a\\.b");
+        assertOutput(
+                "<#ftl outputFormat='dummy' autoEsc=false>#{1.5}; #{1.5; m3}; ${'a.b'}; ${'a.b'?esc}",
+                "1.5; 1.500; a.b; a\\.b");
+        assertOutput("<#ftl outputFormat='plainText'>#{1.5}", "1.5");
+        assertOutput("<#ftl outputFormat='HTML'>#{1.5}", "1.5");
+        assertOutput("#{1.5}", "1.5");
+    }
+    
+    @Test
+    public void testUndefinedOutputFormat() throws IOException, TemplateException {
+        assertOutput("${'a < b'}; ${htmlPlain}; ${htmlMarkup}", "a < b; a &lt; {h&#39;}; <p>c");
+        assertErrorContains("${'x'?esc}", "undefined", "escaping", "?esc");
+        assertErrorContains("${'x'?noEsc}", "undefined", "escaping", "?noEsc");
+    }
+
+    @Test
+    public void testPlainTextOutputFormat() throws IOException, TemplateException {
+        assertOutput("<#ftl outputFormat='plainText'>${'a < b'}; ${htmlPlain}", "a < b; a < {h'}");
+        assertErrorContains("<#ftl outputFormat='plainText'>${htmlMarkup}", "plainText", "HTML", "conversion");
+        assertErrorContains("<#ftl outputFormat='plainText'>${'x'?esc}", "plainText", "escaping", "?esc");
+        assertErrorContains("<#ftl outputFormat='plainText'>${'x'?noEsc}", "plainText", "escaping", "?noEsc");
+    }
+    
+    @Test
+    public void testAutoEscapingOnMOs() throws IOException, TemplateException {
+        for (boolean cfgAutoEscaping : new boolean[] { true, false }) {
+            String commonAutoEscFtl = "<#ftl outputFormat='HTML'>${'&'}";
+            if (cfgAutoEscaping) {
+                // Cfg default is autoEscaping true
+                assertOutput(commonAutoEscFtl, "&amp;");
+            } else {
+                setConfiguration(createDefaultConfigurationBuilder()
+                        .autoEscapingPolicy(DISABLE_AUTO_ESCAPING_POLICY)
+                        .build());
+                assertOutput(commonAutoEscFtl, "&");
+            }
+            
+            assertOutput(
+                    "<#ftl outputFormat='RTF'>"
+                    + "${rtfPlain} ${rtfMarkup} "
+                    + "${htmlPlain} "
+                    + "${xmlPlain}",
+                    "\\\\par a & b \\par c "
+                    + "a < \\{h'\\} "
+                    + "a < \\{x'\\}");
+            assertOutput(
+                    "<#ftl outputFormat='HTML'>"
+                    + "${htmlPlain} ${htmlMarkup} "
+                    + "${xmlPlain} "
+                    + "${rtfPlain}",
+                    "a &lt; {h&#39;} <p>c "
+                    + "a &lt; {x&#39;} "
+                    + "\\par a &amp; b");
+            assertOutput(
+                    "<#ftl outputFormat='XML'>"
+                    + "${xmlPlain} ${xmlMarkup} "
+                    + "${htmlPlain} "
+                    + "${rtfPlain}",
+                    "a &lt; {x&apos;} <p>c</p> "
+                    + "a &lt; {h&apos;} "
+                    + "\\par a &amp; b");
+            assertErrorContains("<#ftl outputFormat='RTF'>${htmlMarkup}", "output format", "RTF", "HTML");
+            assertErrorContains("<#ftl outputFormat='RTF'>${xmlMarkup}", "output format", "RTF", "XML");
+            assertErrorContains("<#ftl outputFormat='HTML'>${rtfMarkup}", "output format", "HTML", "RTF");
+            assertErrorContains("<#ftl outputFormat='HTML'>${xmlMarkup}", "output format", "HTML", "XML");
+            assertErrorContains("<#ftl outputFormat='XML'>${rtfMarkup}", "output format", "XML", "RTF");
+            assertErrorContains("<#ftl outputFormat='XML'>${htmlMarkup}", "output format", "XML", "HTML");
+            
+            for (int hasHeader = 0; hasHeader < 2; hasHeader++) {
+                assertOutput(
+                        (hasHeader == 1 ? "<#ftl outputFormat='undefined'>" : "")
+                        + "${xmlPlain} ${xmlMarkup} "
+                        + "${htmlPlain} ${htmlMarkup} "
+                        + "${rtfPlain} ${rtfMarkup}",
+                        "a &lt; {x&apos;} <p>c</p> "
+                        + "a &lt; {h&#39;} <p>c "
+                        + "\\\\par a & b \\par c");
+            }
+        }
+    }
+
+    @Test
+    public void testStringLiteralsUseUndefinedOF() throws IOException, TemplateException {
+        String expectedOut = "&amp; (&) &amp;";
+        String ftl = "<#ftl outputFormat='XML'>${'&'} ${\"(${'&'})\"?noEsc} ${'&'}";
+        
+        assertOutput(ftl, expectedOut);
+        
+        addTemplate("t.xml", ftl);
+        assertOutputForNamed("t.xml", expectedOut);
+    }
+    
+    @Test
+    public void testUnparsedTemplate() throws IOException, TemplateException {
+        String content = "<#ftl>a<#foo>b${x}";
+        {
+            Template t = Template.createPlainTextTemplate("x", content, getConfiguration());
+            Writer sw = new StringWriter();
+            t.process(null, sw);
+            assertEquals(content, sw.toString());
+            assertEquals(UndefinedOutputFormat.INSTANCE, t.getOutputFormat());
+        }
+        
+        {
+            setConfiguration(new TestConfigurationBuilder().outputFormat(HTMLOutputFormat.INSTANCE).build());
+            Template t = Template.createPlainTextTemplate("x", content, getConfiguration());
+            Writer sw = new StringWriter();
+            t.process(null, sw);
+            assertEquals(content, sw.toString());
+            assertEquals(HTMLOutputFormat.INSTANCE, t.getOutputFormat());
+        }
+    }
+
+    @Test
+    public void testStringLiteralInterpolation() throws IOException, TemplateException {
+        Template t = new Template(null, "<#ftl outputFormat='XML'>${'&'} ${\"(${'&'})\"?noEsc}", getConfiguration());
+        assertEquals(XMLOutputFormat.INSTANCE, t.getOutputFormat());
+        
+        assertOutput("${.outputFormat} ${'${.outputFormat}'} ${.outputFormat}",
+                "undefined undefined undefined");
+        assertOutput("<#ftl outputFormat='HTML'>${.outputFormat} ${'${.outputFormat}'} ${.outputFormat}",
+                "HTML HTML HTML");
+        assertOutput("${.outputFormat} <#outputFormat 'XML'>${'${.outputFormat}'}</#outputFormat> ${.outputFormat}",
+                "undefined XML undefined");
+        assertOutput("${'foo ${xmlPlain}'}", "foo a &lt; {x&apos;}");
+        assertOutput("${'${xmlMarkup}'}", "<p>c</p>");
+        assertErrorContains("${'${\"x\"?esc}'}", "?esc", "undefined");
+        assertOutput("<#ftl outputFormat='XML'>${'${xmlMarkup?esc} ${\"<\"?esc} ${\">\"} ${\"&amp;\"?noEsc}'}",
+                "<p>c</p> &lt; &gt; &amp;");
+    }
+    
+    @Test
+    public void testStringBIsFail() {
+        assertErrorContains("<#ftl outputFormat='HTML'>${'<b>foo</b>'?esc?upperCase}", "string", "markup_output");
+    }
+
+    @Test
+    public void testConcatWithMOs() throws IOException, TemplateException {
+        assertOutput(
+                "${'\\'' + htmlMarkup} ${htmlMarkup + '\\''} ${htmlMarkup + htmlMarkup}",
+                "&#39;<p>c <p>c&#39; <p>c<p>c");
+        assertOutput(
+                "${'\\'' + htmlPlain} ${htmlPlain + '\\''} ${htmlPlain + htmlPlain}",
+                "&#39;a &lt; {h&#39;} a &lt; {h&#39;}&#39; a &lt; {h&#39;}a &lt; {h&#39;}");
+        assertErrorContains(
+                "<#ftl outputFormat='XML'>${'\\'' + htmlMarkup}",
+                "HTML", "XML", "conversion");
+        assertErrorContains(
+                "${xmlMarkup + htmlMarkup}",
+                "HTML", "XML", "Conversion", "common");
+        assertOutput(
+                "${xmlMarkup + htmlPlain}",
+                "<p>c</p>a &lt; {h&apos;}");
+        assertOutput(
+                "${xmlPlain + htmlMarkup}",
+                "a &lt; {x&#39;}<p>c");
+        assertOutput(
+                "${xmlPlain + htmlPlain}",
+                "a &lt; {x&apos;}a &lt; {h&apos;}");
+        assertOutput(
+                "${xmlPlain + htmlPlain + '\\''}",
+                "a &lt; {x&apos;}a &lt; {h&apos;}&apos;");
+        assertOutput(
+                "${htmlPlain + xmlPlain + '\\''}",
+                "a &lt; {h&#39;}a &lt; {x&#39;}&#39;");
+        assertOutput(
+                "${xmlPlain + htmlPlain + '\\''}",
+                "a &lt; {x&apos;}a &lt; {h&apos;}&apos;");
+        assertOutput(
+                "<#ftl outputFormat='XML'>${htmlPlain + xmlPlain + '\\''}",
+                "a &lt; {h&apos;}a &lt; {x&apos;}&apos;");
+        assertOutput(
+                "<#ftl outputFormat='RTF'>${htmlPlain + xmlPlain + '\\''}",
+                "a < \\{h'\\}a < \\{x'\\}'");
+        assertOutput(
+                "<#ftl outputFormat='XML'>${'\\'' + htmlPlain}",
+                "&apos;a &lt; {h&apos;}");
+        assertOutput(
+                "<#ftl outputFormat='HTML'>${'\\'' + htmlPlain}",
+                "&#39;a &lt; {h&#39;}");
+        assertOutput(
+                "<#ftl outputFormat='HTML'>${'\\'' + xmlPlain}",
+                "&#39;a &lt; {x&#39;}");
+        assertOutput(
+                "<#ftl outputFormat='RTF'>${'\\'' + xmlPlain}",
+                "'a < \\{x'\\}");
+        
+        assertOutput(
+                "<#assign x = '\\''><#assign x += xmlMarkup>${x}",
+                "&apos;<p>c</p>");
+        assertOutput(
+                "<#assign x = xmlMarkup><#assign x += '\\''>${x}",
+                "<p>c</p>&apos;");
+        assertOutput(
+                "<#assign x = xmlMarkup><#assign x += htmlPlain>${x}",
+                "<p>c</p>a &lt; {h&apos;}");
+        assertErrorContains(
+                "<#assign x = xmlMarkup><#assign x += htmlMarkup>${x}",
+                "HTML", "XML", "Conversion", "common");
+    }
+    
+    @Test
+    public void testBlockAssignment() throws Exception {
+        for (String d : new String[] { "assign", "global", "local" }) {
+            String commonFTL =
+                    "<#macro m>"
+                    + "<#" + d + " x><p>${'&'}</#" + d + ">${x?isString?c} ${x} ${'&'} "
+                    + "<#" + d + " x></#" + d + ">${x?isString?c}"
+                    + "</#macro><@m />";
+            assertOutput(
+                    commonFTL,
+                    "true <p>& & true");
+            assertOutput(
+                    "<#ftl outputFormat='HTML'>" + commonFTL,
+                    "false <p>&amp; &amp; false");
+        }
+    }
+
+    @Test
+    public void testSpecialVariables() throws Exception {
+        String commonFTL = "${.outputFormat} ${.autoEsc?c}";
+        
+        addTemplate("t.ftlx", commonFTL);
+        assertOutputForNamed("t.ftlx", "XML true");
+        
+        addTemplate("t.ftlh", commonFTL);
+        assertOutputForNamed("t.ftlh", "HTML true");
+
+        addTemplate("t.ftl", commonFTL);
+        assertOutputForNamed("t.ftl", "undefined false");
+        
+        addTemplate("tX.ftl", "<#ftl outputFormat='XML'>" + commonFTL);
+        addTemplate("tX.ftlx", commonFTL);
+        assertOutputForNamed("t.ftlx", "XML true");
+        
+        addTemplate("tN.ftl", "<#ftl outputFormat='RTF' autoEsc=false>" + commonFTL);
+        assertOutputForNamed("tN.ftl", "RTF false");
+        
+        assertOutput("${.output_format} ${.auto_esc?c}", "undefined false");
+    }
+    
+    @Test
+    public void testEscAndNoEscBIBasics() throws IOException, TemplateException {
+        String commonFTL = "${'<x>'} ${'<x>'?esc} ${'<x>'?noEsc}";
+        addTemplate("t.ftlh", commonFTL);
+        addTemplate("t-noAuto.ftlh", "<#ftl autoEsc=false>" + commonFTL);
+        addTemplate("t.ftl", commonFTL);
+        assertOutputForNamed("t.ftlh", "&lt;x&gt; &lt;x&gt; <x>");
+        assertOutputForNamed("t-noAuto.ftlh", "<x> &lt;x&gt; <x>");
+        assertErrorContainsForNamed("t.ftl", "output format", "undefined");
+    }
+
+    @Test
+    public void testEscAndNoEscBIsOnMOs() throws IOException, TemplateException {
+        String xmlHdr = "<#ftl outputFormat='XML'>";
+        
+        assertOutput(
+                xmlHdr + "${'&'?esc?esc} ${'&'?esc?noEsc} ${'&'?noEsc?esc} ${'&'?noEsc?noEsc}",
+                "&amp; &amp; & &");
+        
+        for (String bi : new String[] { "esc", "noEsc" } ) {
+            assertOutput(
+                    xmlHdr + "${rtfPlain?" + bi + "}",
+                    "\\par a &amp; b");
+            assertOutput(
+                    xmlHdr + "<#setting numberFormat='0.0'>${1?" + bi + "}",
+                    "1.0");
+            assertOutput(
+                    xmlHdr + "<#setting booleanFormat='&y,&n'>${true?" + bi + "}",
+                    bi.equals("esc") ? "&amp;y" : "&y");
+            assertErrorContains(
+                    xmlHdr + "${rtfMarkup?" + bi + "}",
+                    "?" + bi, "output format", "RTF", "XML");
+            assertErrorContains(
+                    xmlHdr + "${noSuchVar?" + bi + "}",
+                    "noSuchVar", "null or missing");
+            assertErrorContains(
+                    xmlHdr + "${[]?" + bi + "}",
+                    "?" + bi, "xpected", "string", "sequence");
+        }
+    }
+
+    @Test
+    public void testMarkupStringBI() throws Exception {
+        assertOutput(
+                "${htmlPlain?markupString} ${htmlMarkup?markupString}",
+                "a &lt; {h&#39;} <p>c");
+        assertErrorContains(
+                "${noSuchVar?markupString}",
+                "noSuchVar", "null or missing");
+        assertErrorContains(
+                "${'x'?markupString}",
+                "xpected", "markup output", "string");
+    }
+
+    @Test
+    public void testOutputFormatDirective() throws Exception {
+        assertOutput(
+                "${.outputFormat}${'\\''} "
+                + "<#outputFormat 'HTML'>"
+                + "${.outputFormat}${'\\''} "
+                + "<#outputFormat 'XML'>${.outputFormat}${'\\''}</#outputFormat> "
+                + "${.outputFormat}${'\\''} "
+                + "</#outputFormat>"
+                + "${.outputFormat}${'\\''}",
+                "undefined' HTML&#39; XML&apos; HTML&#39; undefined'");
+        assertOutput(
+                "<#ftl output_format='XML'>"
+                + "${.output_format}${'\\''} "
+                + "<#outputformat 'HTML'>${.output_format}${'\\''}</#outputformat> "
+                + "${.output_format}${'\\''}",
+                "XML&apos; HTML&#39; XML&apos;");
+        
+        // Custom format:
+        assertErrorContains(
+                "<#outputFormat 'dummy'></#outputFormat>",
+                "dummy", "nregistered");
+        setConfiguration(new TestConfigurationBuilder()
+                .registeredCustomOutputFormats(Collections.<OutputFormat>singleton(DummyOutputFormat.INSTANCE))
+                .build());
+        assertOutput(
+                "<#outputFormat 'dummy'>${.outputFormat}</#outputFormat>",
+                "dummy");
+        
+        // Parse-time param expression:
+        assertOutput(
+                "<#outputFormat 'plain' + 'Text'>${.outputFormat}</#outputFormat>",
+                "plainText");
+        assertErrorContains(
+                "<#outputFormat 'plain' + someVar + 'Text'></#outputFormat>",
+                "someVar", "parse-time");
+        assertErrorContains(
+                "<#outputFormat 'plainText'?upperCase></#outputFormat>",
+                "?upperCase", "parse-time");
+        assertErrorContains(
+                "<#outputFormat true></#outputFormat>",
+                "string", "boolean");
+        
+        // Naming convention:
+        assertErrorContains(
+                "<#outputFormat 'HTML'></#outputformat>",
+                "convention", "#outputFormat", "#outputformat");
+        assertErrorContains(
+                "<#outputformat 'HTML'></#outputFormat>",
+                "convention", "#outputFormat", "#outputformat");
+        
+        // Empty block:
+        assertOutput(
+                "${.output_format} "
+                + "<#outputformat 'HTML'></#outputformat>"
+                + "${.output_format}",
+                "undefined undefined");
+        
+        // WS stripping:
+        assertOutput(
+                "${.output_format}\n"
+                + "<#outputformat 'HTML'>\n"
+                + "  x\n"
+                + "</#outputformat>\n"
+                + "${.output_format}",
+                "undefined\n  x\nundefined");
+    }
+
+    @Test
+    public void testAutoEscAndNoAutoEscDirectives() throws Exception {
+        assertOutput(
+                "<#ftl outputFormat='XML'>"
+                + "${.autoEsc?c}${'&'} "
+                + "<#noAutoEsc>"
+                + "${.autoEsc?c}${'&'} "
+                + "<#autoEsc>${.autoEsc?c}${'&'}</#autoEsc> "
+                + "${.autoEsc?c}${'&'} "
+                + "</#noAutoEsc>"
+                + "${.autoEsc?c}${'&'}",
+                "true&amp; false& true&amp; false& true&amp;");
+        assertOutput(
+                "<#ftl auto_esc=false output_format='XML'>"
+                + "${.auto_esc?c}${'&'} "
+                + "<#autoesc>${.auto_esc?c}${'&'}</#autoesc> "
+                + "${.auto_esc?c}${'&'}",
+                "false& true&amp; false&");
+        
+        // Bad came case:
+        assertErrorContains(
+                "<#noAutoesc></#noAutoesc>",
+                "Unknown directive");
+        assertErrorContains(
+                "<#noautoEsc></#noautoEsc>",
+                "Unknown directive");
+
+        setConfiguration(new TestConfigurationBuilder()
+                .outputFormat(XMLOutputFormat.INSTANCE)
+                .build());
+        
+        // Empty block:
+        assertOutput(
+                "${.auto_esc?c} "
+                + "<#noautoesc></#noautoesc>"
+                + "${.auto_esc?c}",
+                "true true");
+        
+        // WS stripping:
+        assertOutput(
+                "${.auto_esc?c}\n"
+                + "<#noautoesc>\n"
+                + "  x\n"
+                + "</#noautoesc>\n"
+                + "${.auto_esc?c}",
+                "true\n  x\ntrue");
+        
+        
+        // Naming convention:
+        assertErrorContains(
+                "<#autoEsc></#autoesc>",
+                "convention", "#autoEsc", "#autoesc");
+        assertErrorContains(
+                "<#autoesc></#autoEsc>",
+                "convention", "#autoEsc", "#autoesc");
+        assertErrorContains(
+                "<#noAutoEsc></#noautoesc>",
+                "convention", "#noAutoEsc", "#noautoesc");
+        assertErrorContains(
+                "<#noautoesc></#noAutoEsc>",
+                "convention", "#noAutoEsc", "#noautoesc");
+    }
+    
+    @Test
+    public void testExplicitAutoEscBannedForNonMarkup() throws Exception {
+        // While this restriction is technically unnecessary, we can catch a dangerous and probably common user
+        // misunderstanding.
+        assertErrorContains("<#ftl autoEsc=true>", "can't do escaping", "undefined");
+        assertErrorContains("<#ftl outputFormat='plainText' autoEsc=true>", "can't do escaping", "plainText");
+        assertErrorContains("<#ftl autoEsc=true outputFormat='plainText'>", "can't do escaping", "plainText");
+        assertOutput("<#ftl autoEsc=true outputFormat='HTML'>", "");
+        assertOutput("<#ftl outputFormat='HTML' autoEsc=true>", "");
+        assertOutput("<#ftl autoEsc=false>", "");
+        
+        assertErrorContains("<#autoEsc></#autoEsc>", "can't do escaping", "undefined");
+        assertErrorContains("<#ftl outputFormat='plainText'><#autoEsc></#autoEsc>", "can't do escaping", "plainText");
+        assertOutput("<#ftl outputFormat='plainText'><#outputFormat 'XML'><#autoEsc></#autoEsc></#outputFormat>", "");
+        assertOutput("<#ftl outputFormat='HTML'><#autoEsc></#autoEsc>", "");
+        assertOutput("<#noAutoEsc></#noAutoEsc>", "");
+    }
+
+    @Test
+    public void testAutoEscPolicy() throws Exception {
+        TestConfigurationBuilder cfgB = createDefaultConfigurationBuilder();
+        cfgB.setRegisteredCustomOutputFormats(ImmutableList.<OutputFormat>of(
+                SeldomEscapedOutputFormat.INSTANCE, DummyOutputFormat.INSTANCE));
+        assertEquals(ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY, cfgB.getAutoEscapingPolicy());
+        
+        String commonFTL = "${'.'} ${.autoEsc?c}";
+        String notEsced = ". false";
+        String esced = "\\. true";
+
+        for (int autoEscPolicy : new int[] {
+                ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY,
+                ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY,
+                DISABLE_AUTO_ESCAPING_POLICY }) {
+            cfgB.setAutoEscapingPolicy(autoEscPolicy);
+            
+            String sExpted = autoEscPolicy == ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY ? esced : notEsced;
+            cfgB.setOutputFormat(SeldomEscapedOutputFormat.INSTANCE);
+            setConfiguration(cfgB.build());
+            assertOutput(commonFTL, sExpted);
+            cfgB.setOutputFormat(UndefinedOutputFormat.INSTANCE);
+            setConfiguration(cfgB.build());
+            assertOutput("<#ftl outputFormat='seldomEscaped'>" + commonFTL, sExpted);
+            assertOutput("<#outputFormat 'seldomEscaped'>" + commonFTL + "</#outputFormat>", sExpted);
+            
+            String dExpted = autoEscPolicy == DISABLE_AUTO_ESCAPING_POLICY ? notEsced : esced;
+            cfgB.setOutputFormat(DummyOutputFormat.INSTANCE);
+            setConfiguration(cfgB.build());
+            assertOutput(commonFTL, dExpted);
+            cfgB.setOutputFormat(UndefinedOutputFormat.INSTANCE);
+            setConfiguration(cfgB.build());
+            assertOutput("<#ftl outputFormat='dummy'>" + commonFTL, dExpted);
+            assertOutput("<#outputFormat 'dummy'>" + commonFTL + "</#outputFormat>", dExpted);
+            
+            cfgB.setOutputFormat(DummyOutputFormat.INSTANCE);
+            setConfiguration(cfgB.build());
+            assertOutput(
+                    commonFTL
+                    + "<#outputFormat 'seldomEscaped'>"
+                        + commonFTL
+                        + "<#outputFormat 'dummy'>"
+                            + commonFTL
+                        + "</#outputFormat>"
+                        + commonFTL
+                        + "<#outputFormat 'plainText'>"
+                            + commonFTL
+                        + "</#outputFormat>"
+                        + commonFTL
+                        + "<#noAutoEsc>"
+                            + commonFTL
+                        + "</#noAutoEsc>"
+                        + commonFTL
+                        + "<#autoEsc>"
+                            + commonFTL
+                        + "</#autoEsc>"
+                        + commonFTL
+                    + "</#outputFormat>"
+                    + commonFTL
+                    + "<#noAutoEsc>"
+                        + commonFTL
+                    + "</#noAutoEsc>"
+                    + commonFTL
+                    + "<#autoEsc>"
+                        + commonFTL
+                    + "</#autoEsc>"
+                    + commonFTL
+                    ,
+                    dExpted
+                        + sExpted
+                            + dExpted
+                        + sExpted
+                            + notEsced
+                        + sExpted
+                            + notEsced
+                        + sExpted
+                            + esced
+                        + sExpted
+                    + dExpted
+                        + notEsced
+                    + dExpted
+                        + esced
+                    + dExpted);
+        }
+    }
+    
+    @Test
+    public void testDynamicParsingBIsInherticContextOutputFormat() throws Exception {
+        // Dynamic parser BI-s are supposed to use the ParsingConfiguration of the calling template, and ignore anything
+        // inside the calling template itself. Except, the outputFormat and autoEscapingPolicy has to come from the
+        // calling lexical context.
+        
+        String commonFTL
+                = "Eval: ${'.outputFormat'?eval}; "
+                  + "Interpret: <#assign ipd = r\"${.outputFormat} ${'{&}'}\"?interpret><@ipd/>";
+        addTemplate("t.ftlh", commonFTL);
+        addTemplate("t2.ftlh", "<#outputFormat 'RTF'>" + commonFTL + "</#outputFormat>");
+        
+        assertOutputForNamed(
+                "t.ftlh",
+                "Eval: HTML; Interpret: HTML {&amp;}");
+        assertOutputForNamed(
+                "t2.ftlh",
+                "Eval: RTF; Interpret: RTF \\{&\\}");
+        assertOutput(
+                commonFTL,
+                "Eval: undefined; Interpret: undefined {&}");
+        assertOutput(
+                "<#ftl outputFormat='RTF'>" + commonFTL + "\n"
+                + "<#outputFormat 'XML'>" + commonFTL + "</#outputFormat>",
+                "Eval: RTF; Interpret: RTF \\{&\\}\n"
+                + "Eval: XML; Interpret: XML {&amp;}");
+        
+        // parser.autoEscapingPolicy is inherited too:
+        assertOutput(
+                "<#ftl autoEsc=false outputFormat='XML'>"
+                + commonFTL + " ${'.autoEsc'?eval?c}",
+                "Eval: XML; Interpret: XML {&} false");
+        assertOutput(
+                "<#ftl outputFormat='XML'>"
+                + "<#noAutoEsc>" + commonFTL + " ${'.autoEsc'?eval?c}</#noAutoEsc>",
+                "Eval: XML; Interpret: XML {&} false");
+        assertOutput(
+                "<#ftl autoEsc=false outputFormat='XML'>"
+                + "<#noAutoEsc>" + commonFTL + " ${'.autoEsc'?eval?c}</#noAutoEsc>",
+                "Eval: XML; Interpret: XML {&} false");
+        assertOutput(
+                "<#ftl autoEsc=false outputFormat='XML'>"
+                + "<#autoEsc>" + commonFTL + " ${'.autoEsc'?eval?c}</#autoEsc>",
+                "Eval: XML; Interpret: XML {&amp;} true");
+        assertOutput(
+                "${.outputFormat}<#assign ftl='<#ftl outputFormat=\\'RTF\\'>$\\{.outputFormat}'> <@ftl?interpret/>",
+                "undefined RTF");
+        assertOutput(
+                "${.outputFormat}<#outputFormat 'RTF'>"
+                + "<#assign ftl='$\\{.outputFormat}'> <@ftl?interpret/> ${'.outputFormat'?eval}"
+                + "</#outputFormat>",
+                "undefined RTF RTF");
+    }
+
+    @Test
+    public void testBannedBIsWhenAutoEscaping() throws Exception {
+        for (String biName : new String[] { "html", "xhtml", "rtf", "xml" }) {
+            String commonFTL = "${'x'?" + biName + "}";
+            assertOutput(commonFTL, "x");
+            assertErrorContains("<#ftl outputFormat='HTML'>" + commonFTL,
+                    "?" + biName, "HTML", "double-escaping");
+            assertErrorContains("<#ftl outputFormat='HTML'>${'${\"x\"?" + biName + "}'}",
+                    "?" + biName, "HTML", "double-escaping");
+            assertOutput("<#ftl outputFormat='plainText'>" + commonFTL, "x");
+            assertOutput("<#ftl outputFormat='HTML' autoEsc=false>" + commonFTL, "x");
+            assertOutput("<#ftl outputFormat='HTML'><#noAutoEsc>" + commonFTL + "</#noAutoEsc>", "x");
+            assertOutput("<#ftl outputFormat='HTML'><#outputFormat 'plainText'>" + commonFTL + "</#outputFormat>",
+                    "x");
+        }
+    }
+
+    @Test
+    public void testLegacyEscaperBIsBypassMOs() throws Exception {
+        assertOutput("${htmlPlain?html} ${htmlMarkup?html}", "a &lt; {h&#39;} <p>c");
+        assertErrorContains("${xmlPlain?html}", "?html", "string", "markup_output", "XML");
+        assertErrorContains("${xmlMarkup?html}", "?html", "string", "markup_output", "XML");
+        assertErrorContains("${rtfPlain?html}", "?html", "string", "markup_output", "RTF");
+        assertErrorContains("${rtfMarkup?html}", "?html", "string", "markup_output", "RTF");
+
+        assertOutput("${htmlPlain?xhtml} ${htmlMarkup?xhtml}", "a &lt; {h&#39;} <p>c");
+        assertErrorContains("${xmlPlain?xhtml}", "?xhtml", "string", "markup_output", "XML");
+        assertErrorContains("${xmlMarkup?xhtml}", "?xhtml", "string", "markup_output", "XML");
+        assertErrorContains("${rtfPlain?xhtml}", "?xhtml", "string", "markup_output", "RTF");
+        assertErrorContains("${rtfMarkup?xhtml}", "?xhtml", "string", "markup_output", "RTF");
+        
+        assertOutput("${xmlPlain?xml} ${xmlMarkup?xml}", "a &lt; {x&apos;} <p>c</p>");
+        assertOutput("${htmlPlain?xml} ${htmlMarkup?xml}", "a &lt; {h&#39;} <p>c");
+        assertErrorContains("${rtfPlain?xml}", "?xml", "string", "markup_output", "RTF");
+        assertErrorContains("${rtfMarkup?xml}", "?xml", "string", "markup_output", "RTF");
+        
+        assertOutput("${rtfPlain?rtf} ${rtfMarkup?rtf}", "\\\\par a & b \\par c");
+        assertErrorContains("${xmlPlain?rtf}", "?rtf", "string", "markup_output", "XML");
+        assertErrorContains("${xmlMarkup?rtf}", "?rtf", "string", "markup_output", "XML");
+        assertErrorContains("${htmlPlain?rtf}", "?rtf", "string", "markup_output", "HTML");
+        assertErrorContains("${htmlMarkup?rtf}", "?rtf", "string", "markup_output", "HTML");
+    }
+    
+    @Test
+    public void testBannedDirectivesWhenAutoEscaping() throws Exception {
+        String commonFTL = "<#escape x as x?html>x</#escape>";
+        assertOutput(commonFTL, "x");
+        assertErrorContains("<#ftl outputFormat='HTML'>" + commonFTL, "escape", "HTML", "double-escaping");
+        assertOutput("<#ftl outputFormat='plainText'>" + commonFTL, "x");
+        assertOutput("<#ftl outputFormat='HTML' autoEsc=false>" + commonFTL, "x");
+        assertOutput("<#ftl outputFormat='HTML'><#noAutoEsc>" + commonFTL + "</#noAutoEsc>", "x");
+        assertOutput("<#ftl outputFormat='HTML'><#outputFormat 'plainText'>" + commonFTL + "</#outputFormat>", "x");
+    }
+    
+    @Test
+    public void testCombinedOutputFormats() throws Exception {
+        assertOutput(
+                "<#outputFormat 'XML{HTML}'>${'\\''}</#outputFormat>",
+                "&amp;#39;");
+        assertOutput(
+                "<#outputFormat 'HTML{RTF{XML}}'>${'<a=\\'{}\\' />'}</#outputFormat>",
+                "&amp;lt;a=&amp;apos;\\{\\}&amp;apos; /&amp;gt;");
+        
+        String commonFtl = "${'\\''} <#outputFormat '{HTML}'>${'\\''}</#outputFormat>";
+        String commonOutput = "&apos; &amp;#39;";
+        assertOutput(
+                "<#outputFormat 'XML'>" + commonFtl + "</#outputFormat>",
+                commonOutput);
+        assertOutput(
+                "<#ftl outputFormat='XML'>" + commonFtl,
+                commonOutput);
+        addTemplate("t.ftlx", commonFtl);
+        assertOutputForNamed(
+                "t.ftlx",
+                commonOutput);
+        
+        assertErrorContains(
+                commonFtl,
+                ParseException.class, "{...}", "markup", UndefinedOutputFormat.INSTANCE.getName());
+        assertErrorContains(
+                "<#ftl outputFormat='plainText'>" + commonFtl,
+                ParseException.class, "{...}", "markup", PlainTextOutputFormat.INSTANCE.getName());
+        assertErrorContains(
+                "<#ftl outputFormat='RTF'><#outputFormat '{plainText}'></#outputFormat>",
+                ParseException.class, "{...}", "markup", PlainTextOutputFormat.INSTANCE.getName());
+        assertErrorContains(
+                "<#ftl outputFormat='RTF'><#outputFormat '{noSuchFormat}'></#outputFormat>",
+                ParseException.class, "noSuchFormat", "registered");
+        assertErrorContains(
+                "<#outputFormat 'noSuchFormat{HTML}'></#outputFormat>",
+                ParseException.class, "noSuchFormat", "registered");
+        assertErrorContains(
+                "<#outputFormat 'HTML{noSuchFormat}'></#outputFormat>",
+                ParseException.class, "noSuchFormat", "registered");
+    }
+    
+    @Test
+    public void testHasContentBI() throws Exception {
+        assertOutput("${htmlMarkup?hasContent?c} ${htmlPlain?hasContent?c}", "true true");
+        assertOutput("<#ftl outputFormat='HTML'>${''?esc?hasContent?c} ${''?noEsc?hasContent?c}", "false false");
+    }
+    
+    @Test
+    public void testMissingVariables() throws Exception {
+        for (String ftl : new String[] {
+                "${noSuchVar}",
+                "<#ftl outputFormat='XML'>${noSuchVar}",
+                "<#ftl outputFormat='XML'>${noSuchVar?esc}",
+                "<#ftl outputFormat='XML'>${'x'?esc + noSuchVar}"
+                }) {
+            assertErrorContains(ftl, InvalidReferenceException.class, "noSuchVar", "null or missing");
+        }
+    }
+
+    @Test
+    public void testIsMarkupOutputBI() throws Exception {
+        addToDataModel("m1", HTMLOutputFormat.INSTANCE.fromPlainTextByEscaping("x"));
+        addToDataModel("m2", HTMLOutputFormat.INSTANCE.fromMarkup("x"));
+        addToDataModel("s", "x");
+        assertOutput("${m1?isMarkupOutput?c} ${m2?isMarkupOutput?c} ${s?isMarkupOutput?c}", "true true false");
+        assertOutput("${m1?is_markup_output?c}", "true");
+    }
+
+    private TestConfigurationBuilder createDefaultConfigurationBuilder() throws TemplateModelException {
+        return new TestConfigurationBuilder()
+                .templateConfigurations(
+                        new ConditionalTemplateConfigurationFactory(
+                                new FileNameGlobMatcher("*.xml"),
+                                new TemplateConfiguration.Builder()
+                                        .outputFormat(XMLOutputFormat.INSTANCE)
+                                        .build()))
+                .cacheStorage(NullCacheStorage.INSTANCE); // Prevent caching as we change the cfgB between build().
+    }
+
+    @Before
+    public void addCommonDataModelVariables() throws TemplateModelException {
+        addToDataModel("rtfPlain", RTFOutputFormat.INSTANCE.fromPlainTextByEscaping("\\par a & b"));
+        addToDataModel("rtfMarkup", RTFOutputFormat.INSTANCE.fromMarkup("\\par c"));
+        addToDataModel("htmlPlain", HTMLOutputFormat.INSTANCE.fromPlainTextByEscaping("a < {h'}"));
+        addToDataModel("htmlMarkup", HTMLOutputFormat.INSTANCE.fromMarkup("<p>c"));
+        addToDataModel("xmlPlain", XMLOutputFormat.INSTANCE.fromPlainTextByEscaping("a < {x'}"));
+        addToDataModel("xmlMarkup", XMLOutputFormat.INSTANCE.fromMarkup("<p>c</p>"));
+    }
+
+    @Override
+    protected Configuration createDefaultConfiguration() throws TemplateModelException {
+        return createDefaultConfigurationBuilder().build();
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/ParseTimeParameterBIErrorMessagesTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/ParseTimeParameterBIErrorMessagesTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/ParseTimeParameterBIErrorMessagesTest.java
new file mode 100644
index 0000000..77b46de
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/ParseTimeParameterBIErrorMessagesTest.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 org.apache.freemarker.test.TemplateTest;
+import org.junit.Test;
+
+public class ParseTimeParameterBIErrorMessagesTest extends TemplateTest {
+
+    @Test
+    public void testThen() throws Exception {
+        assertErrorContains("${true?then}", "expecting", "\"(\"");
+        assertErrorContains("${true?then + 1}", "expecting", "\"(\"");
+        assertErrorContains("${true?then()}", "?then", "2 parameters");
+        assertErrorContains("${true?then(1)}", "?then", "2 parameters");
+        assertOutput("${true?then(1, 2)}", "1");
+        assertErrorContains("${true?then(1, 2, 3)}", "?then", "2 parameters");
+    }
+
+    @Test
+    public void testSwitch() throws Exception {
+        assertErrorContains("${true?switch}", "expecting", "\"(\"");
+        assertErrorContains("${true?switch + 1}", "expecting", "\"(\"");
+        assertErrorContains("${true?switch()}", "at least 2 parameters");
+        assertErrorContains("${true?switch(true)}", "at least 2 parameters");
+        assertOutput("${true?switch(true, 1)}", "1");
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/ParsingErrorMessagesTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/ParsingErrorMessagesTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/ParsingErrorMessagesTest.java
new file mode 100644
index 0000000..8f20d6c
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/ParsingErrorMessagesTest.java
@@ -0,0 +1,116 @@
+/*
+ * 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.junit.Assert.*;
+
+import java.io.IOException;
+
+import org.apache.freemarker.core.util._StringUtil;
+import org.apache.freemarker.test.TestConfigurationBuilder;
+import org.junit.Test;
+
+public class ParsingErrorMessagesTest {
+
+    private Configuration cfg = new TestConfigurationBuilder()
+            .tagSyntax(ParsingConfiguration.AUTO_DETECT_TAG_SYNTAX)
+            .build();
+
+    @Test
+    public void testNeedlessInterpolation() {
+        assertErrorContains("<#if ${x} == 3></#if>", "instead of ${");
+        assertErrorContains("<#if ${x == 3}></#if>", "instead of ${");
+        assertErrorContains("<@foo ${x == 3} />", "instead of ${");
+    }
+
+    @Test
+    public void testWrongDirectiveNames() {
+        assertErrorContains("<#foo />", "nknown directive", "#foo");
+        assertErrorContains("<#set x = 1 />", "nknown directive", "#set", "#assign");
+        assertErrorContains("<#iterator></#iterator>", "nknown directive", "#iterator", "#list");
+    }
+
+    @Test
+    public void testBug402() {
+        assertErrorContains("<#list 1..i as k>${k}<#list>", "existing directive", "malformed", "#list");
+        assertErrorContains("<#assign>", "existing directive", "malformed", "#assign");
+        assertErrorContains("</#if x>", "existing directive", "malformed", "#if");
+        assertErrorContains("<#compress x>", "existing directive", "malformed", "#compress");
+    }
+
+    @Test
+    public void testUnclosedDirectives() {
+        assertErrorContains("<#macro x>", "#macro", "unclosed");
+        assertErrorContains("<#function x>", "#macro", "unclosed");
+        assertErrorContains("<#assign x>", "#assign", "unclosed");
+        assertErrorContains("<#macro m><#local x>", "#local", "unclosed");
+        assertErrorContains("<#global x>", "#global", "unclosed");
+        assertErrorContains("<@foo>", "@...", "unclosed");
+        assertErrorContains("<#list xs as x>", "#list", "unclosed");
+        assertErrorContains("<#list xs as x><#if x>", "#if", "unclosed");
+        assertErrorContains("<#list xs as x><#if x><#if q><#else>", "#if", "unclosed");
+        assertErrorContains("<#list xs as x><#if x><#if q><#else><#macro x>qwe", "#macro", "unclosed");
+        assertErrorContains("${(blah", "\"(\"", "unclosed");
+        assertErrorContains("${blah", "\"{\"", "unclosed");
+    }
+    
+    @Test
+    public void testInterpolatingClosingsErrors() {
+        assertErrorContains("${x", "unclosed");
+        assertErrorContains("<#assign x = x}>", "\"}\"", "open");
+        // TODO assertErrorContains("<#assign x = '${x'>", "unclosed");
+    }
+    
+    private void assertErrorContains(String ftl, String... expectedSubstrings) {
+        assertErrorContains(false, ftl, expectedSubstrings);
+        assertErrorContains(true, ftl, expectedSubstrings);
+    }
+
+    private void assertErrorContains(boolean squareTags, String ftl, String... expectedSubstrings) {
+        try {
+            if (squareTags) {
+                ftl = ftl.replace('<', '[').replace('>', ']');
+            }
+            new Template("adhoc", ftl, cfg);
+            fail("The template had to fail");
+        } catch (ParseException e) {
+            String msg = e.getMessage();
+            for (String needle: expectedSubstrings) {
+                if (needle.startsWith("\\!")) {
+                    String netNeedle = needle.substring(2); 
+                    if (msg.contains(netNeedle)) {
+                        fail("The message shouldn't contain substring " + _StringUtil.jQuote(netNeedle) + ":\n" + msg);
+                    }
+                } else if (!msg.contains(needle)) {
+                    fail("The message didn't contain substring " + _StringUtil.jQuote(needle) + ":\n" + msg);
+                }
+            }
+            showError(e);
+        } catch (IOException e) {
+            // Won't happen
+            throw new RuntimeException(e);
+        }
+    }
+    
+    private void showError(Throwable e) {
+        //System.out.println(e);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/RestrictedObjectWrapperTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/RestrictedObjectWrapperTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/RestrictedObjectWrapperTest.java
new file mode 100644
index 0000000..702a254
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/RestrictedObjectWrapperTest.java
@@ -0,0 +1,72 @@
+/*
+ * 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.test.hamcerst.Matchers.*;
+import static org.junit.Assert.*;
+
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashSet;
+
+import javax.annotation.PostConstruct;
+
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.impl.DefaultArrayAdapter;
+import org.apache.freemarker.core.model.impl.DefaultListAdapter;
+import org.apache.freemarker.core.model.impl.DefaultMapAdapter;
+import org.apache.freemarker.core.model.impl.DefaultNonListCollectionAdapter;
+import org.apache.freemarker.core.model.impl.DefaultObjectWrapperTest.TestBean;
+import org.apache.freemarker.core.model.impl.RestrictedObjectWrapper;
+import org.apache.freemarker.core.model.impl.SimpleDate;
+import org.apache.freemarker.core.model.impl.SimpleNumber;
+import org.apache.freemarker.core.model.impl.SimpleScalar;
+import org.junit.Test;
+
+public class RestrictedObjectWrapperTest {
+
+    @Test
+    public void testBasics() throws TemplateModelException {
+        PostConstruct.class.toString();
+        RestrictedObjectWrapper ow = new RestrictedObjectWrapper.Builder(Configuration.VERSION_3_0_0).build();
+        testCustomizationCommonPart(ow);
+        assertTrue(ow.wrap(Collections.emptyMap()) instanceof DefaultMapAdapter);
+        assertTrue(ow.wrap(Collections.emptyList()) instanceof DefaultListAdapter);
+        assertTrue(ow.wrap(new boolean[] { }) instanceof DefaultArrayAdapter);
+        assertTrue(ow.wrap(new HashSet()) instanceof DefaultNonListCollectionAdapter);
+    }
+
+    @SuppressWarnings("boxing")
+    private void testCustomizationCommonPart(RestrictedObjectWrapper ow) throws TemplateModelException {
+        assertTrue(ow.wrap("x") instanceof SimpleScalar);
+        assertTrue(ow.wrap(1.5) instanceof SimpleNumber);
+        assertTrue(ow.wrap(new Date()) instanceof SimpleDate);
+        assertEquals(TemplateBooleanModel.TRUE, ow.wrap(true));
+        
+        try {
+            ow.wrap(new TestBean());
+            fail();
+        } catch (TemplateModelException e) {
+            assertThat(e.getMessage(), containsStringIgnoringCase("type"));
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/RestrictedObjetWrapperTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/RestrictedObjetWrapperTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/RestrictedObjetWrapperTest.java
new file mode 100644
index 0000000..43ff3bf
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/RestrictedObjetWrapperTest.java
@@ -0,0 +1,112 @@
+/*
+ * 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.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.HashSet;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.apache.freemarker.core.model.TemplateBooleanModel;
+import org.apache.freemarker.core.model.TemplateCollectionModel;
+import org.apache.freemarker.core.model.TemplateCollectionModelEx;
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateHashModelEx2;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateModelWithAPISupport;
+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.model.impl.RestrictedObjectWrapper;
+import org.junit.Test;
+import org.w3c.dom.Document;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+
+public class RestrictedObjetWrapperTest {
+    
+    @Test
+    public void testDoesNotAllowAPIBuiltin() throws TemplateModelException {
+        RestrictedObjectWrapper sow = new RestrictedObjectWrapper.Builder(Configuration.VERSION_3_0_0).build();
+        
+        TemplateModelWithAPISupport map = (TemplateModelWithAPISupport) sow.wrap(new HashMap());
+        try {
+            map.getAPI();
+            fail();
+        } catch (TemplateException e) {
+            assertThat(e.getMessage(), containsString("?api"));
+        }
+    }
+
+    @SuppressWarnings("boxing")
+    @Test
+    public void testCanWrapBasicTypes() throws TemplateModelException {
+        RestrictedObjectWrapper sow = new RestrictedObjectWrapper.Builder(Configuration.VERSION_3_0_0).build();
+        assertTrue(sow.wrap("s") instanceof TemplateScalarModel);
+        assertTrue(sow.wrap(1) instanceof TemplateNumberModel);
+        assertTrue(sow.wrap(true) instanceof TemplateBooleanModel);
+        assertTrue(sow.wrap(new Date()) instanceof TemplateDateModel);
+        assertTrue(sow.wrap(new ArrayList()) instanceof TemplateSequenceModel);
+        assertTrue(sow.wrap(new String[0]) instanceof TemplateSequenceModel);
+        assertTrue(sow.wrap(new ArrayList().iterator()) instanceof TemplateCollectionModel);
+        assertTrue(sow.wrap(new HashSet()) instanceof TemplateCollectionModelEx);
+        assertTrue(sow.wrap(new HashMap()) instanceof TemplateHashModelEx2);
+        assertNull(sow.wrap(null));
+    }
+    
+    @Test
+    public void testWontWrapDOM() throws SAXException, IOException, ParserConfigurationException,
+            TemplateModelException {
+        DocumentBuilder db = DocumentBuilderFactory.newInstance().newDocumentBuilder();
+        InputSource is = new InputSource();
+        is.setCharacterStream(new StringReader("<doc><sub a='1' /></doc>"));
+        Document doc = db.parse(is);
+        
+        RestrictedObjectWrapper sow = new RestrictedObjectWrapper.Builder(Configuration.VERSION_3_0_0).build();
+        try {
+            sow.wrap(doc);
+            fail();
+        } catch (TemplateModelException e) {
+            assertThat(e.getMessage(), containsString("won't wrap"));
+        }
+    }
+    
+    @Test
+    public void testWontWrapGenericObjects() {
+        RestrictedObjectWrapper sow = new RestrictedObjectWrapper.Builder(Configuration.VERSION_3_0_0).build();
+        try {
+            sow.wrap(new File("/x"));
+            fail();
+        } catch (TemplateModelException e) {
+            assertThat(e.getMessage(), containsString("won't wrap"));
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/SQLTimeZoneTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/SQLTimeZoneTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/SQLTimeZoneTest.java
new file mode 100644
index 0000000..cf14b93
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/SQLTimeZoneTest.java
@@ -0,0 +1,371 @@
+/*
+ * 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.junit.Assert.*;
+
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import org.apache.freemarker.core.util._DateUtil;
+import org.apache.freemarker.test.TemplateTest;
+import org.junit.Test;
+
+public class SQLTimeZoneTest extends TemplateTest {
+
+    private final static TimeZone GMT_P02 = TimeZone.getTimeZone("GMT+02");
+    
+    private TimeZone lastDefaultTimeZone;
+
+    private final DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.US);
+    {
+        df.setTimeZone(_DateUtil.UTC);
+    }
+    
+    // Values that JDBC in GMT+02 would produce
+    private final java.sql.Date sqlDate = new java.sql.Date(utcToLong("2014-07-11T22:00:00")); // 2014-07-12
+    private final Time sqlTime = new Time(utcToLong("1970-01-01T10:30:05")); // 12:30:05
+    private final Timestamp sqlTimestamp = new Timestamp(utcToLong("2014-07-12T10:30:05")); // 2014-07-12T12:30:05
+    private final Date javaDate = new Date(utcToLong("2014-07-12T10:30:05")); // 2014-07-12T12:30:05
+    private final Date javaDayErrorDate = new Date(utcToLong("2014-07-11T22:00:00")); // 2014-07-12T12:30:05
+    
+    public TimeZone getLastDefaultTimeZone() {
+        return lastDefaultTimeZone;
+    }
+
+    public void setLastDefaultTimeZone(TimeZone lastDefaultTimeZone) {
+        this.lastDefaultTimeZone = lastDefaultTimeZone;
+    }
+
+    public java.sql.Date getSqlDate() {
+        return sqlDate;
+    }
+
+    public Time getSqlTime() {
+        return sqlTime;
+    }
+
+    public Timestamp getSqlTimestamp() {
+        return sqlTimestamp;
+    }
+
+    public Date getJavaDate() {
+        return javaDate;
+    }
+    
+    public Date getJavaDayErrorDate() {
+        return javaDayErrorDate;
+    }
+
+    private static final String FTL =
+            "${sqlDate} ${sqlTime} ${sqlTimestamp} ${javaDate?datetime}\n"
+            + "${sqlDate?string.iso_fz} ${sqlTime?string.iso_fz} "
+            + "${sqlTimestamp?string.iso_fz} ${javaDate?datetime?string.iso_fz}\n"
+            + "${sqlDate?string.xs_fz} ${sqlTime?string.xs_fz} "
+            + "${sqlTimestamp?string.xs_fz} ${javaDate?datetime?string.xs_fz}\n"
+            + "${sqlDate?string.xs} ${sqlTime?string.xs} "
+            + "${sqlTimestamp?string.xs} ${javaDate?datetime?string.xs}\n"
+            + "<#setting time_zone='GMT'>\n"
+            + "${sqlDate} ${sqlTime} ${sqlTimestamp} ${javaDate?datetime}\n"
+            + "${sqlDate?string.iso_fz} ${sqlTime?string.iso_fz} "
+            + "${sqlTimestamp?string.iso_fz} ${javaDate?datetime?string.iso_fz}\n"
+            + "${sqlDate?string.xs_fz} ${sqlTime?string.xs_fz} "
+            + "${sqlTimestamp?string.xs_fz} ${javaDate?datetime?string.xs_fz}\n"
+            + "${sqlDate?string.xs} ${sqlTime?string.xs} "
+            + "${sqlTimestamp?string.xs} ${javaDate?datetime?string.xs}\n";
+
+    private static final String OUTPUT_BEFORE_SETTING_GMT_CFG_GMT2
+            = "2014-07-12 12:30:05 2014-07-12T12:30:05 2014-07-12T12:30:05\n"
+            + "2014-07-12 12:30:05+02:00 2014-07-12T12:30:05+02:00 2014-07-12T12:30:05+02:00\n"
+            + "2014-07-12+02:00 12:30:05+02:00 2014-07-12T12:30:05+02:00 2014-07-12T12:30:05+02:00\n"
+            + "2014-07-12 12:30:05 2014-07-12T12:30:05+02:00 2014-07-12T12:30:05+02:00\n";
+
+    private static final String OUTPUT_BEFORE_SETTING_GMT_CFG_GMT1_SQL_DIFFERENT
+            = "2014-07-12 12:30:05 2014-07-12T11:30:05 2014-07-12T11:30:05\n"
+            + "2014-07-12 12:30:05+02:00 2014-07-12T11:30:05+01:00 2014-07-12T11:30:05+01:00\n"
+            + "2014-07-12+02:00 12:30:05+02:00 2014-07-12T11:30:05+01:00 2014-07-12T11:30:05+01:00\n"
+            + "2014-07-12 12:30:05 2014-07-12T11:30:05+01:00 2014-07-12T11:30:05+01:00\n";
+
+    private static final String OUTPUT_BEFORE_SETTING_GMT_CFG_GMT1_SQL_SAME
+            = "2014-07-11 11:30:05 2014-07-12T11:30:05 2014-07-12T11:30:05\n"
+            + "2014-07-11 11:30:05+01:00 2014-07-12T11:30:05+01:00 2014-07-12T11:30:05+01:00\n"
+            + "2014-07-11+01:00 11:30:05+01:00 2014-07-12T11:30:05+01:00 2014-07-12T11:30:05+01:00\n"
+            + "2014-07-11 11:30:05 2014-07-12T11:30:05+01:00 2014-07-12T11:30:05+01:00\n";
+    
+    private static final String OUTPUT_AFTER_SETTING_GMT_CFG_SQL_SAME
+            = "2014-07-11 10:30:05 2014-07-12T10:30:05 2014-07-12T10:30:05\n"
+            + "2014-07-11 10:30:05Z 2014-07-12T10:30:05Z 2014-07-12T10:30:05Z\n"
+            + "2014-07-11Z 10:30:05Z 2014-07-12T10:30:05Z 2014-07-12T10:30:05Z\n"
+            + "2014-07-11 10:30:05 2014-07-12T10:30:05Z 2014-07-12T10:30:05Z\n";
+    
+    private static final String OUTPUT_AFTER_SETTING_GMT_CFG_SQL_DIFFERENT
+            = "2014-07-12 12:30:05 2014-07-12T10:30:05 2014-07-12T10:30:05\n"
+            + "2014-07-12 12:30:05+02:00 2014-07-12T10:30:05Z 2014-07-12T10:30:05Z\n"
+            + "2014-07-12+02:00 12:30:05+02:00 2014-07-12T10:30:05Z 2014-07-12T10:30:05Z\n"
+            + "2014-07-12 12:30:05 2014-07-12T10:30:05Z 2014-07-12T10:30:05Z\n";
+    
+    @Test
+    public void testWithDefaultTZAndNullSQL() throws Exception {
+        TimeZone prevSysDefTz = TimeZone.getDefault();
+        TimeZone.setDefault(GMT_P02);
+        try {
+            Configuration.ExtendableBuilder<?> cfgB = createConfigurationBuilder();
+            cfgB.unsetTimeZone();
+            setConfiguration(cfgB.build());
+
+            assertNull(getConfiguration().getSQLDateAndTimeTimeZone());
+            assertEquals(TimeZone.getDefault(), getConfiguration().getTimeZone());
+            
+            assertOutput(FTL, OUTPUT_BEFORE_SETTING_GMT_CFG_GMT2 + OUTPUT_AFTER_SETTING_GMT_CFG_SQL_SAME);
+        } finally {
+            TimeZone.setDefault(prevSysDefTz);
+        }
+    }
+
+    @Test
+    public void testWithDefaultTZAndGMT2SQL() throws Exception {
+        TimeZone prevSysDefTz = TimeZone.getDefault();
+        TimeZone.setDefault(GMT_P02);
+        try {
+            Configuration.ExtendableBuilder<?> cfgB = createConfigurationBuilder();
+            cfgB.sqlDateAndTimeTimeZone(GMT_P02).unsetTimeZone();
+            setConfiguration(cfgB.build());
+
+            assertOutput(FTL, OUTPUT_BEFORE_SETTING_GMT_CFG_GMT2 + OUTPUT_AFTER_SETTING_GMT_CFG_SQL_DIFFERENT);
+        } finally {
+            TimeZone.setDefault(prevSysDefTz);
+        }
+    }
+    
+    @Test
+    public void testWithGMT1AndNullSQL() throws Exception {
+        setConfiguration(createConfigurationBuilder()
+                .timeZone(TimeZone.getTimeZone("GMT+01:00"))
+                .build());
+        assertNull(getConfiguration().getSQLDateAndTimeTimeZone());
+
+        assertOutput(FTL, OUTPUT_BEFORE_SETTING_GMT_CFG_GMT1_SQL_SAME + OUTPUT_AFTER_SETTING_GMT_CFG_SQL_SAME);
+    }
+
+    @Test
+    public void testWithGMT1AndGMT2SQL() throws Exception {
+        setConfiguration(createConfigurationBuilder()
+                .sqlDateAndTimeTimeZone(GMT_P02)
+                .timeZone(TimeZone.getTimeZone("GMT+01:00"))
+                .build());
+
+        assertOutput(FTL, OUTPUT_BEFORE_SETTING_GMT_CFG_GMT1_SQL_DIFFERENT + OUTPUT_AFTER_SETTING_GMT_CFG_SQL_DIFFERENT);
+    }
+
+    @Test
+    public void testWithGMT2AndNullSQL() throws Exception {
+        setConfiguration(createConfigurationBuilder()
+                .timeZone(TimeZone.getTimeZone("GMT+02"))
+                .build());
+        assertNull(getConfiguration().getSQLDateAndTimeTimeZone());
+
+        assertOutput(FTL, OUTPUT_BEFORE_SETTING_GMT_CFG_GMT2 + OUTPUT_AFTER_SETTING_GMT_CFG_SQL_SAME);
+    }
+
+    @Test
+    public void testWithGMT2AndGMT2SQL() throws Exception {
+        setConfiguration(createConfigurationBuilder()
+            .sqlDateAndTimeTimeZone(GMT_P02)
+            .timeZone(TimeZone.getTimeZone("GMT+02"))
+            .build());
+        
+        assertOutput(FTL, OUTPUT_BEFORE_SETTING_GMT_CFG_GMT2 + OUTPUT_AFTER_SETTING_GMT_CFG_SQL_DIFFERENT);
+    }
+    
+    @Test
+    public void testCacheFlushings() throws Exception {
+        Configuration.ExtendableBuilder<?> cfgB = createConfigurationBuilder()
+                .timeZone(_DateUtil.UTC)
+                .dateFormat("yyyy-MM-dd E")
+                .timeFormat("HH:mm:ss E")
+                .dateTimeFormat("yyyy-MM-dd'T'HH:mm:ss E");
+
+        setConfiguration(cfgB.build());
+        assertOutput(
+                "${sqlDate}, ${sqlTime}, ${sqlTimestamp}, ${javaDate?datetime}, ${javaDate?date}, ${javaDate?time}\n"
+                + "<#setting locale='de'>\n"
+                + "${sqlDate}, ${sqlTime}, ${sqlTimestamp}, ${javaDate?datetime}, ${javaDate?date}, ${javaDate?time}\n",
+                "2014-07-11 Fri, 10:30:05 Thu, 2014-07-12T10:30:05 Sat, 2014-07-12T10:30:05 Sat, 2014-07-12 Sat, 10:30:05 Sat\n"
+                + "2014-07-11 Fr, 10:30:05 Do, 2014-07-12T10:30:05 Sa, 2014-07-12T10:30:05 Sa, 2014-07-12 Sa, 10:30:05 Sa\n");
+        assertOutput(
+                "${sqlDate}, ${sqlTime}, ${sqlTimestamp}, ${javaDate?datetime}, ${javaDate?date}, ${javaDate?time}\n"
+                + "<#setting date_format='yyyy-MM-dd'>\n"
+                + "${sqlDate}, ${sqlTime}, ${sqlTimestamp}, ${javaDate?datetime}, ${javaDate?date}, ${javaDate?time}\n",
+                "2014-07-11 Fri, 10:30:05 Thu, 2014-07-12T10:30:05 Sat, 2014-07-12T10:30:05 Sat, 2014-07-12 Sat, 10:30:05 Sat\n"
+                + "2014-07-11, 10:30:05 Thu, 2014-07-12T10:30:05 Sat, 2014-07-12T10:30:05 Sat, 2014-07-12, 10:30:05 Sat\n");
+        assertOutput(
+                "${sqlDate}, ${sqlTime}, ${sqlTimestamp}, ${javaDate?datetime}, ${javaDate?date}, ${javaDate?time}\n"
+                + "<#setting time_format='HH:mm:ss'>\n"
+                + "${sqlDate}, ${sqlTime}, ${sqlTimestamp}, ${javaDate?datetime}, ${javaDate?date}, ${javaDate?time}\n",
+                "2014-07-11 Fri, 10:30:05 Thu, 2014-07-12T10:30:05 Sat, 2014-07-12T10:30:05 Sat, 2014-07-12 Sat, 10:30:05 Sat\n"
+                + "2014-07-11 Fri, 10:30:05, 2014-07-12T10:30:05 Sat, 2014-07-12T10:30:05 Sat, 2014-07-12 Sat, 10:30:05\n");
+        assertOutput(
+                "${sqlDate}, ${sqlTime}, ${sqlTimestamp}, ${javaDate?datetime}, ${javaDate?date}, ${javaDate?time}\n"
+                + "<#setting datetime_format='yyyy-MM-dd\\'T\\'HH:mm:ss'>\n"
+                + "${sqlDate}, ${sqlTime}, ${sqlTimestamp}, ${javaDate?datetime}, ${javaDate?date}, ${javaDate?time}\n",
+                "2014-07-11 Fri, 10:30:05 Thu, 2014-07-12T10:30:05 Sat, 2014-07-12T10:30:05 Sat, 2014-07-12 Sat, 10:30:05 Sat\n"
+                + "2014-07-11 Fri, 10:30:05 Thu, 2014-07-12T10:30:05, 2014-07-12T10:30:05, 2014-07-12 Sat, 10:30:05 Sat\n");
+
+        setConfiguration(cfgB.sqlDateAndTimeTimeZone(GMT_P02).build());
+        assertOutput(
+                "${sqlDate}, ${sqlTime}, ${sqlTimestamp}, ${javaDate?datetime}, ${javaDate?date}, ${javaDate?time}\n"
+                + "<#setting locale='de'>\n"
+                + "${sqlDate}, ${sqlTime}, ${sqlTimestamp}, ${javaDate?datetime}, ${javaDate?date}, ${javaDate?time}\n",
+                "2014-07-12 Sat, 12:30:05 Thu, 2014-07-12T10:30:05 Sat, 2014-07-12T10:30:05 Sat, 2014-07-12 Sat, 10:30:05 Sat\n"
+                + "2014-07-12 Sa, 12:30:05 Do, 2014-07-12T10:30:05 Sa, 2014-07-12T10:30:05 Sa, 2014-07-12 Sa, 10:30:05 Sa\n");
+        assertOutput(
+                "${sqlDate}, ${sqlTime}, ${sqlTimestamp}, ${javaDate?datetime}, ${javaDate?date}, ${javaDate?time}\n"
+                + "<#setting date_format='yyyy-MM-dd'>\n"
+                + "${sqlDate}, ${sqlTime}, ${sqlTimestamp}, ${javaDate?datetime}, ${javaDate?date}, ${javaDate?time}\n",
+                "2014-07-12 Sat, 12:30:05 Thu, 2014-07-12T10:30:05 Sat, 2014-07-12T10:30:05 Sat, 2014-07-12 Sat, 10:30:05 Sat\n"
+                + "2014-07-12, 12:30:05 Thu, 2014-07-12T10:30:05 Sat, 2014-07-12T10:30:05 Sat, 2014-07-12, 10:30:05 Sat\n");
+        assertOutput(
+                "${sqlDate}, ${sqlTime}, ${sqlTimestamp}, ${javaDate?datetime}, ${javaDate?date}, ${javaDate?time}\n"
+                + "<#setting time_format='HH:mm:ss'>\n"
+                + "${sqlDate}, ${sqlTime}, ${sqlTimestamp}, ${javaDate?datetime}, ${javaDate?date}, ${javaDate?time}\n",
+                "2014-07-12 Sat, 12:30:05 Thu, 2014-07-12T10:30:05 Sat, 2014-07-12T10:30:05 Sat, 2014-07-12 Sat, 10:30:05 Sat\n"
+                + "2014-07-12 Sat, 12:30:05, 2014-07-12T10:30:05 Sat, 2014-07-12T10:30:05 Sat, 2014-07-12 Sat, 10:30:05\n");
+        assertOutput(
+                "${sqlDate}, ${sqlTime}, ${sqlTimestamp}, ${javaDate?datetime}, ${javaDate?date}, ${javaDate?time}\n"
+                + "<#setting datetime_format='yyyy-MM-dd\\'T\\'HH:mm:ss'>\n"
+                + "${sqlDate}, ${sqlTime}, ${sqlTimestamp}, ${javaDate?datetime}, ${javaDate?date}, ${javaDate?time}\n",
+                "2014-07-12 Sat, 12:30:05 Thu, 2014-07-12T10:30:05 Sat, 2014-07-12T10:30:05 Sat, 2014-07-12 Sat, 10:30:05 Sat\n"
+                + "2014-07-12 Sat, 12:30:05 Thu, 2014-07-12T10:30:05, 2014-07-12T10:30:05, 2014-07-12 Sat, 10:30:05 Sat\n");
+    }
+
+    @Test
+    public void testDateAndTimeBuiltInsHasNoEffect() throws Exception {
+        setConfiguration(createConfigurationBuilder()
+                .timeZone(_DateUtil.UTC)
+                .sqlDateAndTimeTimeZone(GMT_P02)
+                .build());
+
+        assertOutput(
+                "${javaDayErrorDate?date} ${javaDayErrorDate?time} ${sqlTimestamp?date} ${sqlTimestamp?time} "
+                + "${sqlDate?date} ${sqlTime?time}\n"
+                + "<#setting time_zone='GMT+02'>\n"
+                + "${javaDayErrorDate?date} ${javaDayErrorDate?time} ${sqlTimestamp?date} ${sqlTimestamp?time} "
+                + "${sqlDate?date} ${sqlTime?time}\n"
+                + "<#setting time_zone='GMT-11'>\n"
+                + "${javaDayErrorDate?date} ${javaDayErrorDate?time} ${sqlTimestamp?date} ${sqlTimestamp?time} "
+                + "${sqlDate?date} ${sqlTime?time}\n",
+                "2014-07-11 22:00:00 2014-07-12 10:30:05 2014-07-12 12:30:05\n"
+                + "2014-07-12 00:00:00 2014-07-12 12:30:05 2014-07-12 12:30:05\n"
+                + "2014-07-11 11:00:00 2014-07-11 23:30:05 2014-07-12 12:30:05\n");
+    }
+
+    @Test
+    public void testChangeSettingInTemplate() throws Exception {
+        setConfiguration(createConfigurationBuilder()
+                .timeZone(_DateUtil.UTC)
+                .build());
+
+        assertNull(getConfiguration().getSQLDateAndTimeTimeZone());
+
+        assertOutput(
+                "${sqlDate}, ${sqlTime}, ${sqlTimestamp}, ${javaDate?datetime}\n"
+                + "<#setting sql_date_and_time_time_zone='GMT+02'>\n"
+                + "${sqlDate}, ${sqlTime}, ${sqlTimestamp}, ${javaDate?datetime}\n"
+                + "<#setting sql_date_and_time_time_zone='null'>\n"
+                + "${sqlDate}, ${sqlTime}, ${sqlTimestamp}, ${javaDate?datetime}\n"
+                + "<#setting time_zone='GMT+03'>\n"
+                + "${sqlDate}, ${sqlTime}, ${sqlTimestamp}, ${javaDate?datetime}\n"
+                + "<#setting sql_date_and_time_time_zone='GMT+02'>\n"
+                + "${sqlDate}, ${sqlTime}, ${sqlTimestamp}, ${javaDate?datetime}\n"
+                + "<#setting sql_date_and_time_time_zone='GMT-11'>\n"
+                + "${sqlDate}, ${sqlTime}, ${sqlTimestamp}, ${javaDate?datetime}\n"
+                + "<#setting date_format='xs fz'>\n"
+                + "${sqlDate}, ${sqlTime}, ${sqlTimestamp}, ${javaDate?datetime}\n"
+                + "<#setting time_format='xs fz'>\n"
+                + "${sqlDate}, ${sqlTime}, ${sqlTimestamp}, ${javaDate?datetime}\n"
+                + "<#setting datetime_format='iso m'>\n"
+                + "${sqlDate}, ${sqlTime}, ${sqlTimestamp}, ${javaDate?datetime}\n",
+                "2014-07-11, 10:30:05, 2014-07-12T10:30:05, 2014-07-12T10:30:05\n"
+                + "2014-07-12, 12:30:05, 2014-07-12T10:30:05, 2014-07-12T10:30:05\n"
+                + "2014-07-11, 10:30:05, 2014-07-12T10:30:05, 2014-07-12T10:30:05\n"
+                + "2014-07-12, 13:30:05, 2014-07-12T13:30:05, 2014-07-12T13:30:05\n"
+                + "2014-07-12, 12:30:05, 2014-07-12T13:30:05, 2014-07-12T13:30:05\n"
+                + "2014-07-11, 23:30:05, 2014-07-12T13:30:05, 2014-07-12T13:30:05\n"
+                + "2014-07-11-11:00, 23:30:05, 2014-07-12T13:30:05, 2014-07-12T13:30:05\n"
+                + "2014-07-11-11:00, 23:30:05-11:00, 2014-07-12T13:30:05, 2014-07-12T13:30:05\n"
+                + "2014-07-11-11:00, 23:30:05-11:00, 2014-07-12T13:30+03:00, 2014-07-12T13:30+03:00\n");
+    }
+    
+    @Test
+    public void testFormatUTCFlagHasNoEffect() throws Exception {
+        setConfiguration(createConfigurationBuilder()
+                .sqlDateAndTimeTimeZone(GMT_P02)
+                .timeZone(TimeZone.getTimeZone("GMT-01"))
+                .build());
+        
+        assertOutput(
+                "<#setting date_format='xs fz'><#setting time_format='xs fz'>\n"
+                + "${sqlDate}, ${sqlTime}, ${javaDate?time}\n"
+                + "<#setting date_format='xs fz u'><#setting time_format='xs fz u'>\n"
+                + "${sqlDate}, ${sqlTime}, ${javaDate?time}\n"
+                + "<#setting sql_date_and_time_time_zone='GMT+03'>\n"
+                + "${sqlDate}, ${sqlTime}, ${javaDate?time}\n"
+                + "<#setting sql_date_and_time_time_zone='null'>\n"
+                + "${sqlDate}, ${sqlTime}, ${javaDate?time}\n"
+                + "<#setting date_format='xs fz'><#setting time_format='xs fz'>\n"
+                + "${sqlDate}, ${sqlTime}, ${javaDate?time}\n"
+                + "<#setting date_format='xs fz fu'><#setting time_format='xs fz fu'>\n"
+                + "${sqlDate}, ${sqlTime}, ${javaDate?time}\n",
+                "2014-07-12+02:00, 12:30:05+02:00, 09:30:05-01:00\n"
+                + "2014-07-12+02:00, 12:30:05+02:00, 10:30:05Z\n"
+                + "2014-07-12+03:00, 13:30:05+03:00, 10:30:05Z\n"
+                + "2014-07-11-01:00, 09:30:05-01:00, 10:30:05Z\n"
+                + "2014-07-11-01:00, 09:30:05-01:00, 09:30:05-01:00\n"
+                + "2014-07-11Z, 10:30:05Z, 10:30:05Z\n");
+    }
+
+    private Configuration.ExtendableBuilder<?> createConfigurationBuilder() {
+        return new Configuration.Builder(Configuration.VERSION_3_0_0)
+                .locale(Locale.US)
+                .dateFormat("yyyy-MM-dd")
+                .timeFormat("HH:mm:ss")
+                .dateTimeFormat("yyyy-MM-dd'T'HH:mm:ss");
+    }
+
+    @Override
+    protected Object createDataModel() {
+        return this;
+    }
+
+    private long utcToLong(String isoDateTime) {
+        try {
+            return df.parse(isoDateTime).getTime();
+        } catch (ParseException e) {
+            throw new RuntimeException(e);
+        }
+    }
+    
+}