freemarker-notifications mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ddek...@apache.org
Subject [08/54] [partial] incubator-freemarker git commit: Unifying the o.a.f.core and o.a.f.core.ast
Date Thu, 23 Feb 2017 21:35:35 GMT
http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/test/java/org/apache/freemarker/core/OutputFormatTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/freemarker/core/OutputFormatTest.java b/src/test/java/org/apache/freemarker/core/OutputFormatTest.java
new file mode 100644
index 0000000..0918436
--- /dev/null
+++ b/src/test/java/org/apache/freemarker/core/OutputFormatTest.java
@@ -0,0 +1,1031 @@
+/*
+ * 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.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertTrue;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.io.Writer;
+import java.util.Collections;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.HTMLOutputFormat;
+import org.apache.freemarker.core.InvalidReferenceException;
+import org.apache.freemarker.core.OutputFormat;
+import org.apache.freemarker.core.ParseException;
+import org.apache.freemarker.core.PlainTextOutputFormat;
+import org.apache.freemarker.core.RTFOutputFormat;
+import org.apache.freemarker.core.Template;
+import org.apache.freemarker.core.TemplateConfiguration;
+import org.apache.freemarker.core.TemplateException;
+import org.apache.freemarker.core.UndefinedOutputFormat;
+import org.apache.freemarker.core.XMLOutputFormat;
+import org.apache.freemarker.core.model.TemplateModelException;
+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.test.TemplateTest;
+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}");
+        
+        Configuration cfg = getConfiguration();
+        for (OutputFormat cfgOutputFormat
+                : new OutputFormat[] { UndefinedOutputFormat.INSTANCE, RTFOutputFormat.INSTANCE } ) {
+            if (!cfgOutputFormat.equals(UndefinedOutputFormat.INSTANCE)) {
+                cfg.setOutputFormat(cfgOutputFormat);
+            }
+            
+            assertEquals(cfgOutputFormat, cfg.getOutputFormat());
+            
+            {
+                Template t = cfg.getTemplate("t");
+                assertEquals(cfgOutputFormat, t.getOutputFormat());
+                assertOutput(t, t.getOutputFormat().getName());
+            }
+            
+            {
+                Template t = cfg.getTemplate("t.xml");
+                assertEquals(XMLOutputFormat.INSTANCE, t.getOutputFormat());
+                assertOutput(t, t.getOutputFormat().getName());
+            }
+            
+            {
+                Template t = cfg.getTemplate("tWithHeader");
+                assertEquals(HTMLOutputFormat.INSTANCE, t.getOutputFormat());
+                assertOutput(t, t.getOutputFormat().getName());
+            }
+            
+            cfg.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);
+        
+        Configuration cfg = getConfiguration();
+        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;
+                cfg.setOutputFormat(cfgOutputFormat);
+                ftlhOutputFormat = HTMLOutputFormat.INSTANCE;
+                ftlxOutputFormat = XMLOutputFormat.INSTANCE;
+                break;
+            case 3:
+                cfgOutputFormat = UndefinedOutputFormat.INSTANCE;
+                cfg.unsetOutputFormat();
+                TemplateConfiguration tcXml = new TemplateConfiguration();
+                tcXml.setOutputFormat(XMLOutputFormat.INSTANCE);
+                cfg.setTemplateConfigurations(
+                        new ConditionalTemplateConfigurationFactory(
+                                new OrMatcher(
+                                        new FileNameGlobMatcher("*.ftlh"),
+                                        new FileNameGlobMatcher("*.FTLH"),
+                                        new FileNameGlobMatcher("*.fTlH")),
+                                tcXml));
+                ftlhOutputFormat = HTMLOutputFormat.INSTANCE; // can't be overidden
+                ftlxOutputFormat = XMLOutputFormat.INSTANCE;
+                break;
+            default:
+                throw new AssertionError();
+            }
+            
+            assertEquals(cfgOutputFormat, cfg.getOutputFormat());
+            
+            {
+                Template t = cfg.getTemplate("t");
+                assertEquals(cfgOutputFormat, t.getOutputFormat());
+                assertOutput(t, t.getOutputFormat().getName());
+            }
+            
+            {
+                Template t = cfg.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 = cfg.getTemplate(name);
+                assertEquals(ftlhOutputFormat, t.getOutputFormat());
+                assertOutput(t, t.getOutputFormat().getName());
+            }
+            
+            for (String name : new String[] { "t.ftlx", "t.FTLX", "t.fTlX" }) {
+                Template t = cfg.getTemplate(name);
+                assertEquals(ftlxOutputFormat, t.getOutputFormat());
+                assertOutput(t, t.getOutputFormat().getName());
+            }
+
+            {
+                Template t = cfg.getTemplate("tWithHeader.ftlx");
+                assertEquals(HTMLOutputFormat.INSTANCE, t.getOutputFormat());
+                assertOutput(t, t.getOutputFormat().getName());
+            }
+            
+            cfg.clearTemplateCache();
+        }
+    }
+    
+    @Test
+    public void testStandardFileExtensionsSettingOverriding() throws Exception {
+        addTemplate("t.ftlx",
+                "${\"'\"} ${\"'\"?esc} ${\"'\"?noEsc}");
+        addTemplate("t.ftl",
+                "${'{}'} ${'{}'?esc} ${'{}'?noEsc}");
+        
+        TemplateConfiguration tcHTML = new TemplateConfiguration();
+        tcHTML.setOutputFormat(HTMLOutputFormat.INSTANCE);
+        ConditionalTemplateConfigurationFactory tcfHTML = new ConditionalTemplateConfigurationFactory(
+                new FileNameGlobMatcher("t.*"), tcHTML);
+
+        TemplateConfiguration tcNoAutoEsc = new TemplateConfiguration();
+        tcNoAutoEsc.setAutoEscapingPolicy(Configuration.DISABLE_AUTO_ESCAPING_POLICY);
+        ConditionalTemplateConfigurationFactory tcfNoAutoEsc = new ConditionalTemplateConfigurationFactory(
+                new FileNameGlobMatcher("t.*"), tcNoAutoEsc);
+
+        Configuration cfg = getConfiguration();
+        cfg.setOutputFormat(HTMLOutputFormat.INSTANCE);
+        assertOutputForNamed("t.ftlx", "&apos; &apos; '");  // Can't override it
+        cfg.setTemplateConfigurations(tcfHTML);
+        assertOutputForNamed("t.ftlx", "&apos; &apos; '");  // Can't override it
+        cfg.setTemplateConfigurations(tcfNoAutoEsc);
+        assertOutputForNamed("t.ftlx", "&apos; &apos; '");  // Can't override it
+        
+        cfg.setTemplateConfigurations(null);
+        cfg.unsetOutputFormat();
+        cfg.setRecognizeStandardFileExtensions(false);
+        assertErrorContainsForNamed("t.ftlx", UndefinedOutputFormat.INSTANCE.getName());
+        cfg.setOutputFormat(HTMLOutputFormat.INSTANCE);
+        assertOutputForNamed("t.ftlx", "&#39; &#39; '");
+        cfg.setOutputFormat(XMLOutputFormat.INSTANCE);
+        assertOutputForNamed("t.ftlx", "&apos; &apos; '");
+        cfg.setTemplateConfigurations(tcfHTML);
+        assertOutputForNamed("t.ftlx", "&#39; &#39; '");
+        cfg.setTemplateConfigurations(tcfNoAutoEsc);
+        assertOutputForNamed("t.ftlx", "' &apos; '");
+        
+        cfg.setRecognizeStandardFileExtensions(true);
+        cfg.setTemplateConfigurations(tcfHTML);
+        assertOutputForNamed("t.ftlx", "&apos; &apos; '");  // Can't override it
+        cfg.setTemplateConfigurations(tcfNoAutoEsc);
+        assertOutputForNamed("t.ftlx", "&apos; &apos; '");  // Can't override it
+        
+        cfg.setTemplateConfigurations(null);
+        cfg.unsetOutputFormat();
+        cfg.setTemplateConfigurations(tcfHTML);
+        assertOutputForNamed("t.ftlx", "&apos; &apos; '");  // Can't override it
+        cfg.setRecognizeStandardFileExtensions(false);
+        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");
+        getConfiguration().setRegisteredCustomOutputFormats(Collections.singleton(CustomHTMLOutputFormat.INSTANCE));
+        assertOutputForNamed("t.ftlh", "a&amp;X");
+        getConfiguration().setRegisteredCustomOutputFormats(Collections.<OutputFormat>emptyList());
+        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'}");
+        
+        Configuration cfg = getConfiguration();
+        
+        assertEquals(Configuration.ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY, cfg.getAutoEscapingPolicy());
+        
+        cfg.setOutputFormat(XMLOutputFormat.INSTANCE);
+        
+        for (boolean cfgAutoEscaping : new boolean[] { true, false }) {
+            if (!cfgAutoEscaping) {
+                cfg.setAutoEscapingPolicy(Configuration.DISABLE_AUTO_ESCAPING_POLICY);
+            }
+            
+            {
+                Template t = cfg.getTemplate("t");
+                if (cfgAutoEscaping) {
+                    assertTrue(t.getAutoEscaping());
+                    assertOutput(t, "a&amp;b");
+                } else {
+                    assertFalse(t.getAutoEscaping());
+                    assertOutput(t, "a&b");
+                }
+            }
+            
+            {
+                Template t = cfg.getTemplate("tWithHeaderFalse");
+                assertFalse(t.getAutoEscaping());
+                assertOutput(t, "a&b");
+            }
+            
+            {
+                Template t = cfg.getTemplate("tWithHeaderTrue");
+                assertTrue(t.getAutoEscaping());
+                assertOutput(t, "a&amp;b");
+            }
+            
+            cfg.clearTemplateCache();
+        }
+    }
+    
+    @Test
+    public void testNumericalInterpolation() throws IOException, TemplateException {
+        getConfiguration().setRegisteredCustomOutputFormats(Collections.singleton(DummyOutputFormat.INSTANCE));
+        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 (int autoEsc = 0; autoEsc < 2; autoEsc++) {
+            String commonAutoEscFtl = "<#ftl outputFormat='HTML'>${'&'}";
+            if (autoEsc == 0) {
+                // Cfg default is autoEscaping true
+                assertOutput(commonAutoEscFtl, "&amp;");
+            } else {
+                getConfiguration().setAutoEscapingPolicy(Configuration.DISABLE_AUTO_ESCAPING_POLICY);
+                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.getPlainTextTemplate("x", content, getConfiguration());
+            Writer sw = new StringWriter();
+            t.process(null, sw);
+            assertEquals(content, sw.toString());
+            assertEquals(UndefinedOutputFormat.INSTANCE, t.getOutputFormat());
+        }
+        
+        {
+            getConfiguration().setOutputFormat(HTMLOutputFormat.INSTANCE);
+            Template t = Template.getPlainTextTemplate("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");
+        getConfiguration().setRegisteredCustomOutputFormats(Collections.singleton(DummyOutputFormat.INSTANCE));
+        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");
+
+        getConfiguration().setOutputFormat(XMLOutputFormat.INSTANCE);
+        
+        // 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 {
+        Configuration cfg = getConfiguration();
+        cfg.setRegisteredCustomOutputFormats(ImmutableList.of(
+                SeldomEscapedOutputFormat.INSTANCE, DummyOutputFormat.INSTANCE));
+        assertEquals(Configuration.ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY, cfg.getAutoEscapingPolicy());
+        
+        String commonFTL = "${'.'} ${.autoEsc?c}";
+        String notEsced = ". false";
+        String esced = "\\. true";
+
+        for (int autoEscPolicy : new int[] {
+                Configuration.ENABLE_IF_DEFAULT_AUTO_ESCAPING_POLICY,
+                Configuration.ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY,
+                Configuration.DISABLE_AUTO_ESCAPING_POLICY }) {
+            cfg.setAutoEscapingPolicy(autoEscPolicy);
+            
+            String sExpted = autoEscPolicy == Configuration.ENABLE_IF_SUPPORTED_AUTO_ESCAPING_POLICY ? esced : notEsced;
+            cfg.setOutputFormat(SeldomEscapedOutputFormat.INSTANCE);
+            assertOutput(commonFTL, sExpted);
+            cfg.setOutputFormat(UndefinedOutputFormat.INSTANCE);
+            assertOutput("<#ftl outputFormat='seldomEscaped'>" + commonFTL, sExpted);
+            assertOutput("<#outputFormat 'seldomEscaped'>" + commonFTL + "</#outputFormat>", sExpted);
+            
+            String dExpted = autoEscPolicy == Configuration.DISABLE_AUTO_ESCAPING_POLICY ? notEsced : esced;
+            cfg.setOutputFormat(DummyOutputFormat.INSTANCE);
+            assertOutput(commonFTL, dExpted);
+            cfg.setOutputFormat(UndefinedOutputFormat.INSTANCE);
+            assertOutput("<#ftl outputFormat='dummy'>" + commonFTL, dExpted);
+            assertOutput("<#outputFormat 'dummy'>" + commonFTL + "</#outputFormat>", dExpted);
+            
+            cfg.setOutputFormat(DummyOutputFormat.INSTANCE);
+            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 parserConfiguration of the calling template, and ignore anything
+        // inside the calling template itself. Except, the outputFormat 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" }) {
+            getConfiguration().setIncompatibleImprovements(Configuration.VERSION_3_0_0);
+            
+            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");
+    }
+    
+    @Override
+    protected Configuration createConfiguration() throws TemplateModelException {
+        Configuration cfg = new Configuration(Configuration.VERSION_3_0_0);
+        
+        TemplateConfiguration xmlTC = new TemplateConfiguration();
+        xmlTC.setOutputFormat(XMLOutputFormat.INSTANCE);
+        cfg.setTemplateConfigurations(
+                new ConditionalTemplateConfigurationFactory(new FileNameGlobMatcher("*.xml"), xmlTC));
+
+        cfg.setSharedVariable("rtfPlain", RTFOutputFormat.INSTANCE.fromPlainTextByEscaping("\\par a & b"));
+        cfg.setSharedVariable("rtfMarkup", RTFOutputFormat.INSTANCE.fromMarkup("\\par c"));
+        cfg.setSharedVariable("htmlPlain", HTMLOutputFormat.INSTANCE.fromPlainTextByEscaping("a < {h'}"));
+        cfg.setSharedVariable("htmlMarkup", HTMLOutputFormat.INSTANCE.fromMarkup("<p>c"));
+        cfg.setSharedVariable("xmlPlain", XMLOutputFormat.INSTANCE.fromPlainTextByEscaping("a < {x'}"));
+        cfg.setSharedVariable("xmlMarkup", XMLOutputFormat.INSTANCE.fromMarkup("<p>c</p>"));
+        
+        return cfg;
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/test/java/org/apache/freemarker/core/ParseTimeParameterBIErrorMessagesTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/freemarker/core/ParseTimeParameterBIErrorMessagesTest.java b/src/test/java/org/apache/freemarker/core/ParseTimeParameterBIErrorMessagesTest.java
new file mode 100644
index 0000000..cbeabc5
--- /dev/null
+++ b/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/7d784b2b/src/test/java/org/apache/freemarker/core/ParsingErrorMessagesTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/freemarker/core/ParsingErrorMessagesTest.java b/src/test/java/org/apache/freemarker/core/ParsingErrorMessagesTest.java
new file mode 100644
index 0000000..99af659
--- /dev/null
+++ b/src/test/java/org/apache/freemarker/core/ParsingErrorMessagesTest.java
@@ -0,0 +1,119 @@
+/*
+ * 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.fail;
+
+import java.io.IOException;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.ParseException;
+import org.apache.freemarker.core.Template;
+import org.apache.freemarker.core.util._StringUtil;
+import org.junit.Test;
+
+public class ParsingErrorMessagesTest {
+
+    private Configuration cfg = new Configuration(Configuration.VERSION_3_0_0);
+    {
+        cfg.setTagSyntax(Configuration.AUTO_DETECT_TAG_SYNTAX);
+    }
+    
+    @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 tempalte 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/7d784b2b/src/test/java/org/apache/freemarker/core/PrintfGTemplateNumberFormatFactory.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/freemarker/core/PrintfGTemplateNumberFormatFactory.java b/src/test/java/org/apache/freemarker/core/PrintfGTemplateNumberFormatFactory.java
new file mode 100644
index 0000000..0408b48
--- /dev/null
+++ b/src/test/java/org/apache/freemarker/core/PrintfGTemplateNumberFormatFactory.java
@@ -0,0 +1,138 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ * 
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * 
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.freemarker.core;
+
+import java.math.BigDecimal;
+import java.math.BigInteger;
+import java.util.Locale;
+
+import org.apache.freemarker.core.Environment;
+import org.apache.freemarker.core.HTMLOutputFormat;
+import org.apache.freemarker.core.InvalidFormatParametersException;
+import org.apache.freemarker.core.TemplateFormatUtil;
+import org.apache.freemarker.core.TemplateNumberFormat;
+import org.apache.freemarker.core.TemplateNumberFormatFactory;
+import org.apache.freemarker.core.UnformattableValueException;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+import org.apache.freemarker.core.util._StringUtil;
+
+/**
+ * Formats like {@code %G} in {@code printf}, with the specified number of significant digits. Also has special
+ * formatter for HTML output format, where it uses the HTML "sup" element for exponents.
+ */
+public class PrintfGTemplateNumberFormatFactory extends TemplateNumberFormatFactory {
+
+    public static final PrintfGTemplateNumberFormatFactory INSTANCE = new PrintfGTemplateNumberFormatFactory();
+    
+    private PrintfGTemplateNumberFormatFactory() {
+        // Defined to decrease visibility
+    }
+    
+    @Override
+    public TemplateNumberFormat get(String params, Locale locale, Environment env)
+            throws InvalidFormatParametersException {
+        Integer significantDigits;
+        if (!params.isEmpty()) {
+            try {
+                significantDigits = Integer.valueOf(params);
+            } catch (NumberFormatException e) {
+                throw new InvalidFormatParametersException(
+                        "The format parameter must be an integer, but was (shown quoted) "
+                        + _StringUtil.jQuote(params) + ".");
+            }
+        } else {
+            // Use the default of %G
+            significantDigits = null;
+        }
+        return new PrintfGTemplateNumberFormat(significantDigits, locale);
+    }
+
+    private static class PrintfGTemplateNumberFormat extends TemplateNumberFormat {
+        
+        private final Locale locale;
+        private final String printfFormat; 
+
+        private PrintfGTemplateNumberFormat(Integer significantDigits, Locale locale) {
+            printfFormat = "%" + (significantDigits != null ? "." + significantDigits : "") + "G";
+            this.locale = locale;
+        }
+        
+        @Override
+        public String formatToPlainText(TemplateNumberModel numberModel)
+                throws UnformattableValueException, TemplateModelException {
+            final Number n = TemplateFormatUtil.getNonNullNumber(numberModel);
+            
+            // printf %G only accepts Double, BigDecimal and Float 
+            final Number gCompatibleN;
+            if (n instanceof Double  || n instanceof BigDecimal || n instanceof Float) {
+                gCompatibleN = n;
+            } else {
+                if (n instanceof BigInteger) {
+                    gCompatibleN = new BigDecimal((BigInteger) n);                        
+                } else if (n instanceof Long) {
+                    gCompatibleN = BigDecimal.valueOf(n.longValue());
+                } else {
+                    gCompatibleN = Double.valueOf(n.doubleValue());
+                }
+            }
+            
+            return String.format(locale, printfFormat, gCompatibleN);
+        }
+
+        @Override
+        public Object format(TemplateNumberModel numberModel)
+                throws UnformattableValueException, TemplateModelException {
+            String strResult = formatToPlainText(numberModel);
+            
+            int expIdx = strResult.indexOf('E');
+            if (expIdx == -1) {
+                return strResult;
+            }
+                
+            String expStr = strResult.substring(expIdx + 1);
+            int expSignifNumBegin = 0;
+            while (expSignifNumBegin < expStr.length() && isExpSignifNumPrefix(expStr.charAt(expSignifNumBegin))) {
+                expSignifNumBegin++;
+            }
+            
+            return HTMLOutputFormat.INSTANCE.fromMarkup(
+                    strResult.substring(0, expIdx)
+                    + "*10<sup>"
+                    + (expStr.charAt(0) == '-' ? "-" : "") + expStr.substring(expSignifNumBegin)
+                    + "</sup>");
+        }
+
+        private boolean isExpSignifNumPrefix(char c) {
+            return c == '+' || c == '-' || c == '0';
+        }
+
+        @Override
+        public boolean isLocaleBound() {
+            return true;
+        }
+
+        @Override
+        public String getDescription() {
+            return "printf " + printfFormat;
+        }
+        
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/test/java/org/apache/freemarker/core/RTFOutputFormatTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/freemarker/core/RTFOutputFormatTest.java b/src/test/java/org/apache/freemarker/core/RTFOutputFormatTest.java
new file mode 100644
index 0000000..3b2c878
--- /dev/null
+++ b/src/test/java/org/apache/freemarker/core/RTFOutputFormatTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.RTFOutputFormat.*;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.StringWriter;
+
+import org.apache.freemarker.core.TemplateRTFOutputModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.junit.Test; 
+
+public class RTFOutputFormatTest {
+    
+    @Test
+    public void testOutputMO() throws TemplateModelException, IOException {
+       StringWriter out = new StringWriter();
+       
+       INSTANCE.output(INSTANCE.fromMarkup("\\par Test "), out);
+       INSTANCE.output(INSTANCE.fromPlainTextByEscaping("foo { bar } \\ "), out);
+       INSTANCE.output(INSTANCE.fromPlainTextByEscaping("baaz "), out);
+       INSTANCE.output(INSTANCE.fromPlainTextByEscaping("\\par qweqwe"), out);
+       INSTANCE.output(INSTANCE.fromMarkup("\\par{0} End"), out);
+       
+       assertEquals(
+               "\\par Test "
+               + "foo \\{ bar \\} \\\\ "
+               + "baaz "
+               + "\\\\par qweqwe"
+               + "\\par{0} End",
+               out.toString());
+    }
+    
+    @Test
+    public void testOutputString() throws TemplateModelException, IOException {
+        StringWriter out = new StringWriter();
+        
+        INSTANCE.output("a", out);
+        INSTANCE.output("{", out);
+        INSTANCE.output("b}c", out);
+        
+        assertEquals("a\\{b\\}c", out.toString());
+    }
+    
+    @Test
+    public void testFromPlainTextByEscaping() throws TemplateModelException {
+        String plainText = "a\\b";
+        TemplateRTFOutputModel mo = INSTANCE.fromPlainTextByEscaping(plainText);
+        assertSame(plainText, mo.getPlainTextContent());
+        assertNull(mo.getMarkupContent()); // Not the MO's duty to calculate it!
+    }
+
+    @Test
+    public void testFromMarkup() throws TemplateModelException {
+        String markup = "a \\par b";
+        TemplateRTFOutputModel mo = INSTANCE.fromMarkup(markup);
+        assertSame(markup, mo.getMarkupContent());
+        assertNull(mo.getPlainTextContent()); // Not the MO's duty to calculate it!
+    }
+    
+    @Test
+    public void testGetMarkup() throws TemplateModelException {
+        {
+            String markup = "a \\par b";
+            TemplateRTFOutputModel mo = INSTANCE.fromMarkup(markup);
+            assertSame(markup, INSTANCE.getMarkupString(mo));
+        }
+        
+        {
+            String safe = "abc";
+            TemplateRTFOutputModel mo = INSTANCE.fromPlainTextByEscaping(safe);
+            assertSame(safe, INSTANCE.getMarkupString(mo));
+        }
+    }
+    
+    @Test
+    public void testConcat() throws Exception {
+        assertMO(
+                "ab", null,
+                INSTANCE.concat(new TemplateRTFOutputModel("a", null), new TemplateRTFOutputModel("b", null)));
+        assertMO(
+                null, "ab",
+                INSTANCE.concat(new TemplateRTFOutputModel(null, "a"), new TemplateRTFOutputModel(null, "b")));
+        assertMO(
+                null, "{a}\\{b\\}",
+                INSTANCE.concat(new TemplateRTFOutputModel(null, "{a}"), new TemplateRTFOutputModel("{b}", null)));
+        assertMO(
+                null, "\\{a\\}{b}",
+                INSTANCE.concat(new TemplateRTFOutputModel("{a}", null), new TemplateRTFOutputModel(null, "{b}")));
+    }
+    
+    @Test
+    public void testEscaplePlainText() {
+        assertEquals("", INSTANCE.escapePlainText(""));
+        assertEquals("a", INSTANCE.escapePlainText("a"));
+        assertEquals("\\{a\\\\b\\}", INSTANCE.escapePlainText("{a\\b}"));
+        assertEquals("a\\\\b", INSTANCE.escapePlainText("a\\b"));
+        assertEquals("\\{\\}", INSTANCE.escapePlainText("{}"));
+    }
+    
+    private void assertMO(String pc, String mc, TemplateRTFOutputModel mo) {
+        assertEquals(pc, mo.getPlainTextContent());
+        assertEquals(mc, mo.getMarkupContent());
+    }
+    
+    @Test
+    public void testGetMimeType() {
+        assertEquals("application/rtf", INSTANCE.getMimeType());
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/7d784b2b/src/test/java/org/apache/freemarker/core/SQLTimeZoneTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/freemarker/core/SQLTimeZoneTest.java b/src/test/java/org/apache/freemarker/core/SQLTimeZoneTest.java
new file mode 100644
index 0000000..ba18c08
--- /dev/null
+++ b/src/test/java/org/apache/freemarker/core/SQLTimeZoneTest.java
@@ -0,0 +1,362 @@
+/*
+ * 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.assertEquals;
+import static org.junit.Assert.assertNull;
+
+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.Configuration;
+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 cfg = getConfiguration();
+            assertNull(cfg.getSQLDateAndTimeTimeZone());
+            assertEquals(TimeZone.getDefault(), cfg.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 cfg = getConfiguration();
+            cfg.setSQLDateAndTimeTimeZone(GMT_P02);
+            
+            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 {
+        Configuration cfg = getConfiguration();
+        assertNull(cfg.getSQLDateAndTimeTimeZone());
+        cfg.setTimeZone(TimeZone.getTimeZone("GMT+01:00"));
+        
+        assertOutput(FTL, OUTPUT_BEFORE_SETTING_GMT_CFG_GMT1_SQL_SAME + OUTPUT_AFTER_SETTING_GMT_CFG_SQL_SAME);
+    }
+
+    @Test
+    public void testWithGMT1AndGMT2SQL() throws Exception {
+        Configuration cfg = getConfiguration();
+        cfg.setSQLDateAndTimeTimeZone(GMT_P02);
+        cfg.setTimeZone(TimeZone.getTimeZone("GMT+01:00"));
+        
+        assertOutput(FTL, OUTPUT_BEFORE_SETTING_GMT_CFG_GMT1_SQL_DIFFERENT + OUTPUT_AFTER_SETTING_GMT_CFG_SQL_DIFFERENT);
+    }
+
+    @Test
+    public void testWithGMT2AndNullSQL() throws Exception {
+        Configuration cfg = getConfiguration();
+        assertNull(cfg.getSQLDateAndTimeTimeZone());
+        cfg.setTimeZone(TimeZone.getTimeZone("GMT+02"));
+        
+        assertOutput(FTL, OUTPUT_BEFORE_SETTING_GMT_CFG_GMT2 + OUTPUT_AFTER_SETTING_GMT_CFG_SQL_SAME);
+    }
+
+    @Test
+    public void testWithGMT2AndGMT2SQL() throws Exception {
+        Configuration cfg = getConfiguration();
+        cfg.setSQLDateAndTimeTimeZone(GMT_P02);
+        cfg.setTimeZone(TimeZone.getTimeZone("GMT+02"));
+        
+        assertOutput(FTL, OUTPUT_BEFORE_SETTING_GMT_CFG_GMT2 + OUTPUT_AFTER_SETTING_GMT_CFG_SQL_DIFFERENT);
+    }
+    
+    @Test
+    public void testCacheFlushings() throws Exception {
+        Configuration cfg = getConfiguration();
+        cfg.setTimeZone(_DateUtil.UTC);
+        cfg.setDateFormat("yyyy-MM-dd E");
+        cfg.setTimeFormat("HH:mm:ss E");
+        cfg.setDateTimeFormat("yyyy-MM-dd'T'HH:mm:ss E");
+        
+        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");
+        
+        cfg.setSQLDateAndTimeTimeZone(GMT_P02);
+        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 {
+        Configuration cfg = getConfiguration();
+        cfg.setTimeZone(_DateUtil.UTC);
+        cfg.setSQLDateAndTimeTimeZone(GMT_P02);
+        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 {
+        Configuration cfg = getConfiguration();
+        cfg.setTimeZone(_DateUtil.UTC);
+        
+        assertNull(cfg.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 {
+        Configuration cfg = getConfiguration();
+        cfg.setSQLDateAndTimeTimeZone(GMT_P02);
+        cfg.setTimeZone(TimeZone.getTimeZone("GMT-01"));
+        
+        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");
+    }
+    
+    @Override
+    protected Configuration createConfiguration() {
+        Configuration cfg = new Configuration(Configuration.VERSION_3_0_0);
+        cfg.setLocale(Locale.US);
+        cfg.setDateFormat("yyyy-MM-dd");
+        cfg.setTimeFormat("HH:mm:ss");
+        cfg.setDateTimeFormat("yyyy-MM-dd'T'HH:mm:ss");
+        return cfg;
+    }
+    
+    @Override
+    protected Object createDataModel() {
+        return this;
+    }
+
+    private long utcToLong(String isoDateTime) {
+        try {
+            return df.parse(isoDateTime).getTime();
+        } catch (ParseException e) {
+            throw new RuntimeException(e);
+        }
+    }
+    
+}


Mime
View raw message