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:23:58 UTC

[42/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/TemplateGetEncodingTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateGetEncodingTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateGetEncodingTest.java
new file mode 100644
index 0000000..4b5bf59
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateGetEncodingTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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 java.nio.charset.Charset;
+
+import org.apache.freemarker.core.templateresolver.ConditionalTemplateConfigurationFactory;
+import org.apache.freemarker.core.templateresolver.FileNameGlobMatcher;
+import org.apache.freemarker.core.templateresolver.impl.StrongCacheStorage;
+import org.apache.freemarker.test.MonitoredTemplateLoader;
+import org.junit.Test;
+
+public class TemplateGetEncodingTest {
+
+    private static final Charset ISO_8859_2 = Charset.forName("ISO-8859-2");
+
+    @Test
+    public void test() throws IOException {
+        Configuration.Builder cfgB = new Configuration.Builder(Configuration.VERSION_3_0_0);
+        {
+            cfgB.setSourceEncoding(ISO_8859_2);
+            MonitoredTemplateLoader tl = new MonitoredTemplateLoader();
+            tl.putBinaryTemplate("bin", "test");
+            tl.putBinaryTemplate("bin-static", "<#test>");
+            tl.putTextTemplate("text", "test");
+            tl.putTextTemplate("text-static", "<#test>");
+            TemplateConfiguration.Builder staticTextTCB = new TemplateConfiguration.Builder();
+            staticTextTCB.setTemplateLanguage(TemplateLanguage.STATIC_TEXT);
+            cfgB.setTemplateConfigurations(
+                    new ConditionalTemplateConfigurationFactory(
+                            new FileNameGlobMatcher("*-static*"), staticTextTCB.build()));
+            cfgB.setTemplateLoader(tl);
+            cfgB.setCacheStorage(new StrongCacheStorage());
+        }
+
+        Configuration cfg = cfgB.build();
+        assertEquals(ISO_8859_2, cfg.getTemplate("bin").getActualSourceEncoding());
+        assertEquals(ISO_8859_2, cfg.getTemplate("bin-static").getActualSourceEncoding());
+        assertNull(cfg.getTemplate("text").getActualSourceEncoding());
+        assertNull(cfg.getTemplate("text-static").getActualSourceEncoding());
+        assertNull(new Template(null, "test", cfg).getActualSourceEncoding());
+        assertNull(Template.createPlainTextTemplate(null, "<#test>", cfg).getActualSourceEncoding());
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateLookupStrategyTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateLookupStrategyTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateLookupStrategyTest.java
new file mode 100644
index 0000000..f0e63a8
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateLookupStrategyTest.java
@@ -0,0 +1,669 @@
+/*
+ * 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.Configuration.ExtendableBuilder.TEMPLATE_LOOKUP_STRATEGY_KEY;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.Serializable;
+import java.io.StringWriter;
+import java.util.Locale;
+
+import org.apache.freemarker.core.templateresolver.TemplateLookupContext;
+import org.apache.freemarker.core.templateresolver.TemplateLookupResult;
+import org.apache.freemarker.core.templateresolver.TemplateLookupStrategy;
+import org.apache.freemarker.core.templateresolver.impl.DefaultTemplateLookupStrategy;
+import org.apache.freemarker.test.MonitoredTemplateLoader;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+
+public class TemplateLookupStrategyTest {
+
+    @Test
+    public void testSetSetting() throws Exception {
+        assertSame(
+                DefaultTemplateLookupStrategy.INSTANCE,
+                new Configuration.Builder(Configuration.VERSION_3_0_0).build()
+                        .getTemplateLookupStrategy());
+
+        assertTrue(
+                new Configuration.Builder(Configuration.VERSION_3_0_0)
+                        .setting(TEMPLATE_LOOKUP_STRATEGY_KEY, MyTemplateLookupStrategy.class.getName() + "()")
+                        .build()
+                        .getTemplateLookupStrategy() instanceof MyTemplateLookupStrategy);
+        
+        assertSame(
+                DefaultTemplateLookupStrategy.INSTANCE,
+                new Configuration.Builder(Configuration.VERSION_3_0_0)
+                        .setting(TEMPLATE_LOOKUP_STRATEGY_KEY, "dEfault")
+                        .build()
+                        .getTemplateLookupStrategy());
+    }
+    
+    @Test
+    public void testCustomStrategy() throws IOException {
+        MonitoredTemplateLoader tl = new MonitoredTemplateLoader();
+        tl.putTextTemplate("test.ftl", "");
+        tl.putTextTemplate("aa/test.ftl", "");
+
+        Configuration cfg = new Configuration.Builder(Configuration.VERSION_3_0_0)
+                .templateLoader(tl)
+                .templateLookupStrategy(MyTemplateLookupStrategy.INSTANCE)
+                .build();
+        
+        final Locale locale = new Locale("aa", "BB", "CC_DD");
+        
+        try {
+            cfg.getTemplate("missing.ftl", locale);
+            fail();
+        } catch (TemplateNotFoundException e) {
+            assertEquals("missing.ftl", e.getTemplateName());
+            assertEquals(ImmutableList.of("aa/missing.ftl", "missing.ftl"), tl.getLoadNames());
+            tl.clearEvents();
+            cfg.clearTemplateCache();
+        }
+        
+        {
+            final Template t = cfg.getTemplate("test.ftl", locale);
+            assertEquals("test.ftl", t.getLookupName());
+            assertEquals("aa/test.ftl", t.getSourceName());
+            assertEquals(locale, t.getLocale());
+            assertNull(t.getCustomLookupCondition());
+            assertEquals(ImmutableList.of("aa/test.ftl"), tl.getLoadNames());
+            tl.clearEvents();
+            cfg.clearTemplateCache();
+        }
+    }
+    
+    @Test
+    public void testDefaultStrategy() throws IOException {
+        MonitoredTemplateLoader tl = new MonitoredTemplateLoader();
+        tl.putTextTemplate("test.ftl", "");
+        tl.putTextTemplate("test_aa.ftl", "");
+        tl.putTextTemplate("test_aa_BB.ftl", "");
+        tl.putTextTemplate("test_aa_BB_CC.ftl", "");
+        tl.putTextTemplate("test_aa_BB_CC_DD.ftl", "");
+
+        try {
+            new Configuration.Builder(Configuration.VERSION_3_0_0).templateLoader(tl).build()
+                    .getTemplate("missing.ftl", new Locale("aa", "BB", "CC_DD"));
+            fail();
+        } catch (TemplateNotFoundException e) {
+            assertEquals("missing.ftl", e.getTemplateName());
+            assertEquals(
+                    ImmutableList.of(
+                            "missing_aa_BB_CC_DD.ftl",
+                            "missing_aa_BB_CC.ftl",
+                            "missing_aa_BB.ftl",
+                            "missing_aa.ftl",
+                            "missing.ftl"),
+                    tl.getLoadNames());
+            tl.clearEvents();
+        }
+
+        try {
+            new Configuration.Builder(Configuration.VERSION_3_0_0).templateLoader(tl)
+                    .locale(new Locale("xx")).build()
+                    .getTemplate("missing.ftl");
+            fail();
+        } catch (TemplateNotFoundException e) {
+            assertEquals("missing.ftl", e.getTemplateName());
+            assertEquals(
+                    ImmutableList.of("missing_xx.ftl", "missing.ftl"),
+                    tl.getLoadNames());
+            tl.clearEvents();
+        }
+        
+        try {
+            new Configuration.Builder(Configuration.VERSION_3_0_0).templateLoader(tl)
+                    .locale(new Locale("xx"))
+                    .localizedLookup(false).build()
+                    .getTemplate("missing.ftl");
+            fail();
+        } catch (TemplateNotFoundException e) {
+            assertEquals("missing.ftl", e.getTemplateName());
+            assertEquals(
+                    ImmutableList.of("missing.ftl"),
+                    tl.getLoadNames());
+            tl.clearEvents();
+        }
+
+        try {
+            new Configuration.Builder(Configuration.VERSION_3_0_0).templateLoader(tl).build()
+                    .getTemplate("_a_b_.ftl", new Locale("xx", "yy"));
+            fail();
+        } catch (TemplateNotFoundException e) {
+            assertEquals("_a_b_.ftl", e.getTemplateName());
+            assertEquals(
+                    ImmutableList.of("_a_b__xx_YY.ftl", "_a_b__xx.ftl", "_a_b_.ftl"),
+                    tl.getLoadNames());
+            tl.clearEvents();
+        }
+
+        for (String templateName : new String[] { "test.ftl", "./test.ftl", "/test.ftl", "x/foo/../../test.ftl" }) {
+            {
+                final Locale locale = new Locale("aa", "BB", "CC_DD");
+                final Template t = new Configuration.Builder(Configuration.VERSION_3_0_0).templateLoader(tl).build()
+                        .getTemplate("test.ftl", locale);
+                assertEquals("test.ftl", t.getLookupName());
+                assertEquals("test_aa_BB_CC_DD.ftl", t.getSourceName());
+                assertEquals(locale, t.getLocale());
+                assertNull(t.getCustomLookupCondition());
+                assertEquals(ImmutableList.of("test_aa_BB_CC_DD.ftl"), tl.getLoadNames());
+                assertNull(t.getCustomLookupCondition());
+                tl.clearEvents();
+            }
+            
+            {
+                final Locale locale = new Locale("aa", "BB", "CC_XX");
+                final Template t = new Configuration.Builder(Configuration.VERSION_3_0_0).templateLoader(tl).build()
+                        .getTemplate(templateName, locale);
+                assertEquals("test.ftl", t.getLookupName());
+                assertEquals("test_aa_BB_CC.ftl", t.getSourceName());
+                assertEquals(locale, t.getLocale());
+                assertNull(t.getCustomLookupCondition());
+                assertEquals(ImmutableList.of("test_aa_BB_CC_XX.ftl", "test_aa_BB_CC.ftl"), tl.getLoadNames());
+                tl.clearEvents();
+            }
+            
+            {
+                final Locale locale = new Locale("aa", "BB", "XX_XX");
+                final Template t = new Configuration.Builder(Configuration.VERSION_3_0_0).templateLoader(tl).build()
+                        .getTemplate(templateName, locale);
+                assertEquals("test.ftl", t.getLookupName());
+                assertEquals("test_aa_BB.ftl", t.getSourceName());
+                assertEquals(locale, t.getLocale());
+                assertNull(t.getCustomLookupCondition());
+                assertEquals(
+                        ImmutableList.of("test_aa_BB_XX_XX.ftl", "test_aa_BB_XX.ftl", "test_aa_BB.ftl"),
+                        tl.getLoadNames());
+                tl.clearEvents();
+            }
+    
+            {
+                final Locale locale = new Locale("aa", "BB", "XX_XX");
+                final Template t = new Configuration.Builder(Configuration.VERSION_3_0_0).templateLoader(tl)
+                        .localizedLookup(false).build()
+                        .getTemplate(templateName, locale);
+                assertEquals("test.ftl", t.getLookupName());
+                assertEquals("test.ftl", t.getSourceName());
+                assertEquals(locale, t.getLocale());
+                assertNull(t.getCustomLookupCondition());
+                assertEquals(
+                        ImmutableList.of("test.ftl"),
+                        tl.getLoadNames());
+                tl.clearEvents();
+            }
+    
+            {
+                final Locale locale = new Locale("aa", "XX", "XX_XX");
+                final Template t = new Configuration.Builder(Configuration.VERSION_3_0_0).templateLoader(tl).build()
+                        .getTemplate(templateName, locale);
+                assertEquals("test.ftl", t.getLookupName());
+                assertEquals("test_aa.ftl", t.getSourceName());
+                assertEquals(locale, t.getLocale());
+                assertNull(t.getCustomLookupCondition());
+                assertEquals(
+                        ImmutableList.of("test_aa_XX_XX_XX.ftl", "test_aa_XX_XX.ftl", "test_aa_XX.ftl", "test_aa.ftl"),
+                        tl.getLoadNames());
+                tl.clearEvents();
+            }
+            
+            {
+                final Locale locale = new Locale("xx", "XX", "XX_XX");
+                final Template t = new Configuration.Builder(Configuration.VERSION_3_0_0).templateLoader(tl).build()
+                        .getTemplate(templateName, locale);
+                assertEquals("test.ftl", t.getLookupName());
+                assertEquals("test.ftl", t.getSourceName());
+                assertEquals(locale, t.getLocale());
+                assertNull(t.getCustomLookupCondition());
+                assertEquals(
+                        ImmutableList.of(
+                                "test_xx_XX_XX_XX.ftl", "test_xx_XX_XX.ftl", "test_xx_XX.ftl", "test_xx.ftl", "test.ftl"),
+                        tl.getLoadNames());
+                tl.clearEvents();
+            }
+            
+            {
+                final Locale locale = new Locale("xx", "BB", "CC_DD");
+                final Template t = new Configuration.Builder(Configuration.VERSION_3_0_0).templateLoader(tl).build()
+                        .getTemplate(templateName, locale);
+                assertEquals("test.ftl", t.getLookupName());
+                assertEquals("test.ftl", t.getSourceName());
+                assertEquals(locale, t.getLocale());
+                assertNull(t.getCustomLookupCondition());
+                assertEquals(
+                        ImmutableList.of(
+                            "test_xx_BB_CC_DD.ftl", "test_xx_BB_CC.ftl", "test_xx_BB.ftl", "test_xx.ftl", "test.ftl"),
+                        tl.getLoadNames());
+                tl.clearEvents();
+            }
+        }
+    }
+    
+    @Test
+    public void testAcquisition() throws IOException {
+        MonitoredTemplateLoader tl = new MonitoredTemplateLoader();
+        tl.putTextTemplate("t.ftl", "");
+        tl.putTextTemplate("sub/i.ftl", "");
+        tl.putTextTemplate("x/sub/i.ftl", "");
+
+        Configuration cfg = new Configuration.Builder(Configuration.VERSION_3_0_0).templateLoader(tl).build();
+
+        final Locale locale = new Locale("xx");
+        
+        {
+            final Template t = cfg.getTemplate("/./moo/../x/y/*/sub/i.ftl", locale);
+            assertEquals("x/y/*/sub/i.ftl", t.getLookupName());
+            assertEquals("x/sub/i.ftl", t.getSourceName());
+            assertEquals(locale, t.getLocale());
+            assertNull(t.getCustomLookupCondition());
+            assertEquals(
+                    ImmutableList.of(
+                        "x/y/sub/i_xx.ftl", "x/sub/i_xx.ftl", "sub/i_xx.ftl",
+                        "x/y/sub/i.ftl", "x/sub/i.ftl"),
+                    tl.getLoadNames());
+            tl.clearEvents();
+            cfg.clearTemplateCache();
+        }
+
+        {
+            final Template t = cfg.getTemplate("a/b/*/./sub/i.ftl", locale);
+            assertEquals("a/b/*/sub/i.ftl", t.getLookupName());
+            assertEquals("sub/i.ftl", t.getSourceName());
+            assertEquals(locale, t.getLocale());
+            assertNull(t.getCustomLookupCondition());
+            assertEquals(
+                    ImmutableList.of(
+                        "a/b/sub/i_xx.ftl", "a/sub/i_xx.ftl", "sub/i_xx.ftl",
+                        "a/b/sub/i.ftl", "a/sub/i.ftl", "sub/i.ftl"),
+                    tl.getLoadNames());
+            tl.clearEvents();
+            cfg.clearTemplateCache();
+        }
+    }
+
+    @Test
+    public void testCustomLookupCondition() throws IOException, TemplateException {
+        MonitoredTemplateLoader tl = new MonitoredTemplateLoader();
+
+        final Configuration cfg;
+        final Configuration cfgNoLocLU;
+        {
+            Configuration.Builder cfgB = new Configuration.Builder(Configuration.VERSION_3_0_0)
+                    .templateLoader(tl)
+                    .templateLookupStrategy(new DomainTemplateLookupStrategy());
+            cfg = cfgB.build();
+            cfgNoLocLU = cfgB.localizedLookup(false).build();
+        }
+
+        final String iAtDefaultContent = "i at default";
+        final String iXxAtDefaultContent = "i_xx at default";
+        final String iAtBaazComContent = "i at baaz.com";
+        final String iAtFooComContent = "i at foo.com";
+        final String tAtDefaultWithoutIncludeContent = "t at default ";
+        final String tAtDefaultContent = toCanonicalFTL(tAtDefaultWithoutIncludeContent + "<#include 'i.ftl'>", cfg);
+        final String tAtBarComWithoutIncludeContent = "t at bar.com ";
+        final String tAtBarComContent = toCanonicalFTL(tAtBarComWithoutIncludeContent + "<#include 'i.ftl'>", cfg);
+        final String tAtFooComWithoutIncludeContent = "t at foo.com ";
+        final String tAtFooComContent = toCanonicalFTL(tAtFooComWithoutIncludeContent + "<#include 'i.ftl'>", cfg);
+        final String t2XxLocaleExpectedOutput = "i3_xx at foo.com";
+        final String t2OtherLocaleExpectedOutput = "i3 at foo.com";
+        
+        tl.putTextTemplate("@foo.com/t.ftl", tAtFooComContent);
+        tl.putTextTemplate("@bar.com/t.ftl", tAtBarComContent);
+        tl.putTextTemplate("@default/t.ftl", tAtDefaultContent);
+        tl.putTextTemplate("@foo.com/i.ftl", iAtFooComContent);
+        tl.putTextTemplate("@baaz.com/i.ftl", iAtBaazComContent);
+        tl.putTextTemplate("@default/i_xx.ftl", iXxAtDefaultContent);
+        tl.putTextTemplate("@default/i.ftl", iAtDefaultContent);
+        tl.putTextTemplate("@foo.com/t2.ftl", "<#import 'i2.ftl' as i2 />${proof}");
+        tl.putTextTemplate("@default/i2.ftl", "<#import 'i3.ftl' as i3 />");
+        tl.putTextTemplate("@foo.com/i3.ftl", "<#global proof = '" + t2OtherLocaleExpectedOutput + "'>");
+        tl.putTextTemplate("@foo.com/i3_xx.ftl", "<#global proof = '" + t2XxLocaleExpectedOutput + "'>");
+
+        {
+            final Locale locale = new Locale("xx");
+            final Domain domain = new Domain("foo.com");
+            final Template t = cfg.getTemplate("t.ftl", locale, domain);
+            assertEquals("t.ftl", t.getLookupName());
+            assertEquals("@foo.com/t.ftl", t.getSourceName());
+            assertEquals(locale, t.getLocale());
+            assertEquals(domain, t.getCustomLookupCondition());
+            assertEquals(tAtFooComContent, t.toString());
+            assertEquals(
+                    ImmutableList.of("@foo.com/t_xx.ftl", "@foo.com/t.ftl"),
+                    tl.getLoadNames());
+            
+            tl.clearEvents();
+            assertOutputEquals(tAtFooComWithoutIncludeContent + iAtFooComContent, t);
+            assertEquals(
+                    ImmutableList.of("@foo.com/i_xx.ftl", "@foo.com/i.ftl"),
+                    tl.getLoadNames());
+            
+            tl.clearEvents();
+            cfg.clearTemplateCache();
+        }
+
+        {
+            final Locale locale = new Locale("xx");
+            final Domain domain = new Domain("bar.com");
+            final Template t = cfg.getTemplate("t.ftl", locale, domain);
+            assertEquals("t.ftl", t.getLookupName());
+            assertEquals("@bar.com/t.ftl", t.getSourceName());
+            assertEquals(locale, t.getLocale());
+            assertEquals(domain, t.getCustomLookupCondition());
+            assertEquals(tAtBarComContent, t.toString());
+            assertEquals(
+                    ImmutableList.of("@bar.com/t_xx.ftl", "@bar.com/t.ftl"),
+                    tl.getLoadNames());
+            
+            tl.clearEvents();
+            assertOutputEquals(tAtBarComWithoutIncludeContent + iXxAtDefaultContent, t);
+            assertEquals(
+                    ImmutableList.of(
+                            "@bar.com/i_xx.ftl", "@bar.com/i.ftl",
+                            "@default/i_xx.ftl"),
+                    tl.getLoadNames());
+            
+            tl.clearEvents();
+            cfg.clearTemplateCache();
+        }
+        
+        {
+            final Locale locale = new Locale("xx", "YY");
+            final Domain domain = new Domain("baaz.com");
+            final Template t = cfg.getTemplate("t.ftl", locale, domain);
+            assertEquals("t.ftl", t.getLookupName());
+            assertEquals("@default/t.ftl", t.getSourceName());
+            assertEquals(locale, t.getLocale());
+            assertEquals(domain, t.getCustomLookupCondition());
+            assertEquals(tAtDefaultContent, t.toString());
+            assertEquals(
+                    ImmutableList.of(
+                            "@baaz.com/t_xx_YY.ftl", "@baaz.com/t_xx.ftl", "@baaz.com/t.ftl",
+                            "@default/t_xx_YY.ftl", "@default/t_xx.ftl", "@default/t.ftl"),
+                    tl.getLoadNames());
+            
+            tl.clearEvents();
+            assertOutputEquals(tAtDefaultWithoutIncludeContent + iAtBaazComContent, t);
+            assertEquals(
+                    ImmutableList.of("@baaz.com/i_xx_YY.ftl", "@baaz.com/i_xx.ftl", "@baaz.com/i.ftl"),
+                    tl.getLoadNames());
+            
+            tl.clearEvents();
+            cfg.clearTemplateCache();
+        }
+        
+        {
+            final Locale locale = new Locale("xx", "YY");
+            final Domain domain = new Domain("nosuch.com");
+            final Template t = cfg.getTemplate("i.ftl", locale, domain);
+            assertEquals("i.ftl", t.getLookupName());
+            assertEquals("@default/i_xx.ftl", t.getSourceName());
+            assertEquals(locale, t.getLocale());
+            assertEquals(domain, t.getCustomLookupCondition());
+            assertEquals(iXxAtDefaultContent, t.toString());
+            assertEquals(
+                    ImmutableList.of(
+                            "@nosuch.com/i_xx_YY.ftl", "@nosuch.com/i_xx.ftl", "@nosuch.com/i.ftl",
+                            "@default/i_xx_YY.ftl", "@default/i_xx.ftl"),
+                    tl.getLoadNames());
+            
+            tl.clearEvents();
+            cfg.clearTemplateCache();
+        }
+
+        {
+            final Locale locale = new Locale("xx", "YY");
+            final Domain domain = new Domain("nosuch.com");
+            final Template t = cfgNoLocLU.getTemplate("i.ftl", locale, domain);
+            assertEquals("i.ftl", t.getLookupName());
+            assertEquals("@default/i.ftl", t.getSourceName());
+            assertEquals(locale, t.getLocale());
+            assertEquals(domain, t.getCustomLookupCondition());
+            assertEquals(iAtDefaultContent, t.toString());
+            assertEquals(
+                    ImmutableList.of("@nosuch.com/i.ftl", "@default/i.ftl"),
+                    tl.getLoadNames());
+            
+            tl.clearEvents();
+            cfgNoLocLU.clearTemplateCache();
+        }
+        
+        {
+            final Locale locale = new Locale("xx");
+            final Domain domain = new Domain("foo.com");
+            final Template t = cfg.getTemplate("t2.ftl", locale, domain);
+            assertOutputEquals(t2XxLocaleExpectedOutput, t);
+            assertEquals(
+                    ImmutableList.of(
+                            "@foo.com/t2_xx.ftl", "@foo.com/t2.ftl",
+                            "@foo.com/i2_xx.ftl", "@foo.com/i2.ftl", "@default/i2_xx.ftl", "@default/i2.ftl",
+                            "@foo.com/i3_xx.ftl"),
+                    tl.getLoadNames());
+            
+            tl.clearEvents();
+            cfg.clearTemplateCache();
+        }
+        
+        {
+            final Locale locale = new Locale("yy");
+            final Domain domain = new Domain("foo.com");
+            final Template t = cfg.getTemplate("t2.ftl", locale, domain);
+            assertOutputEquals(t2OtherLocaleExpectedOutput, t);
+            assertEquals(
+                    ImmutableList.of(
+                            "@foo.com/t2_yy.ftl", "@foo.com/t2.ftl",
+                            "@foo.com/i2_yy.ftl", "@foo.com/i2.ftl", "@default/i2_yy.ftl", "@default/i2.ftl",
+                            "@foo.com/i3_yy.ftl", "@foo.com/i3.ftl"),
+                    tl.getLoadNames());
+            
+            tl.clearEvents();
+            cfg.clearTemplateCache();
+        }
+        
+        {
+            final Locale locale = new Locale("xx");
+            final Domain domain = new Domain("foo.com");
+            final Template t = cfgNoLocLU.getTemplate("t2.ftl", locale, domain);
+            assertOutputEquals(t2OtherLocaleExpectedOutput, t);
+            assertEquals(
+                    ImmutableList.of(
+                            "@foo.com/t2.ftl",
+                            "@foo.com/i2.ftl", "@default/i2.ftl",
+                            "@foo.com/i3.ftl"),
+                    tl.getLoadNames());
+            
+            tl.clearEvents();
+            cfgNoLocLU.clearTemplateCache();
+        }
+        
+        {
+            final Locale locale = new Locale("xx");
+            final Domain domain = new Domain("foo.com");
+            cfg.getTemplate("i3.ftl", locale, domain);
+            assertEquals(
+                    ImmutableList.of("@foo.com/i3_xx.ftl"),
+                    tl.getLoadNames());
+            
+            tl.clearEvents();
+            cfg.clearTemplateCache();
+        }
+        
+        {
+            final Locale locale = new Locale("xx");
+            final Domain domain = new Domain("bar.com");
+            try {
+                cfg.getTemplate("i3.ftl", locale, domain);
+            } catch (TemplateNotFoundException e) {
+                assertEquals("i3.ftl", e.getTemplateName());
+                assertEquals(domain, e.getCustomLookupCondition());
+            }
+            assertEquals(
+                    ImmutableList.of(
+                            "@bar.com/i3_xx.ftl", "@bar.com/i3.ftl",
+                            "@default/i3_xx.ftl", "@default/i3.ftl"),
+                    tl.getLoadNames());
+            
+            tl.clearEvents();
+            cfg.clearTemplateCache();
+        }
+        
+    }
+
+    public static class Domain implements Serializable {
+        private final String name;
+
+        public Domain(String name) {
+            this.name = name;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+
+            Domain domain = (Domain) o;
+
+            return name != null ? name.equals(domain.name) : domain.name == null;
+        }
+
+        @Override
+        public int hashCode() {
+            return name != null ? name.hashCode() : 0;
+        }
+    }
+    
+    @Test
+    public void testNonparsed() throws IOException {
+        MonitoredTemplateLoader tl = new MonitoredTemplateLoader();
+        tl.putTextTemplate("test.txt", "");
+        tl.putTextTemplate("test_aa.txt", "");
+
+        Configuration cfg = new Configuration.Builder(Configuration.VERSION_3_0_0).templateLoader(tl).build();
+
+        try {
+            cfg.getTemplate("missing.txt", new Locale("aa", "BB"), null, false);
+            fail();
+        } catch (TemplateNotFoundException e) {
+            assertEquals("missing.txt", e.getTemplateName());
+            assertEquals(
+                    ImmutableList.of(
+                            "missing_aa_BB.txt",
+                            "missing_aa.txt",
+                            "missing.txt"),
+                    tl.getLoadNames());
+            tl.clearEvents();
+            cfg.clearTemplateCache();
+        }
+        
+        {
+            Template t = cfg.getTemplate("test.txt", new Locale("aa", "BB"), null, false);
+            assertEquals("test.txt", t.getLookupName());
+            assertEquals("test_aa.txt", t.getSourceName());
+            assertEquals(
+                    ImmutableList.of(
+                            "test_aa_BB.txt",
+                            "test_aa.txt"),
+                    tl.getLoadNames());
+        }
+    }
+
+    @Test
+    public void testParseError() throws IOException {
+        MonitoredTemplateLoader tl = new MonitoredTemplateLoader();
+        tl.putTextTemplate("test.ftl", "");
+        tl.putTextTemplate("test_aa.ftl", "<#wrong>");
+
+        Configuration cfg = new Configuration.Builder(Configuration.VERSION_3_0_0).templateLoader(tl).build();
+        
+        try {
+            cfg.getTemplate("test.ftl", new Locale("aa", "BB"));
+            fail();
+        } catch (ParseException e) {
+            assertEquals("test_aa.ftl", e.getTemplateSourceName());
+            assertEquals("test.ftl", e.getTemplateLookupName());
+        }
+    }
+    
+    private String toCanonicalFTL(String ftl, Configuration cfg) throws IOException {
+        return new Template(null, ftl, cfg).toString();        
+    }
+
+    private void assertOutputEquals(final String expectedContent, final Template t) throws TemplateException,
+            IOException {
+        StringWriter sw = new StringWriter(); 
+        t.process(null, sw);
+        assertEquals(expectedContent, sw.toString());
+    }
+    
+    public static class MyTemplateLookupStrategy extends TemplateLookupStrategy {
+        
+        public static final MyTemplateLookupStrategy INSTANCE = new MyTemplateLookupStrategy();
+        
+        private MyTemplateLookupStrategy() { }
+
+        @Override
+        public <R extends TemplateLookupResult> R lookup(TemplateLookupContext<R> ctx) throws IOException {
+            String lang = ctx.getTemplateLocale().getLanguage().toLowerCase();
+            R lookupResult = ctx.lookupWithAcquisitionStrategy(lang + "/" + ctx.getTemplateName());
+            if (lookupResult.isPositive()) {
+                return lookupResult;
+            }
+            
+            return ctx.lookupWithAcquisitionStrategy(ctx.getTemplateName());
+        }
+        
+    }
+    
+    public static class DomainTemplateLookupStrategy extends TemplateLookupStrategy {
+        
+        public static final DomainTemplateLookupStrategy INSTANCE = new DomainTemplateLookupStrategy();
+
+        @Override
+        public <R extends TemplateLookupResult> R lookup(TemplateLookupContext<R> ctx) throws IOException {
+            Domain domain = (Domain) ctx.getCustomLookupCondition();
+            if (domain == null) {
+                throw new NullPointerException("The domain wasn't specified");
+            }
+            
+            final String templateName = ctx.getTemplateName();
+            
+            // Disallow addressing the domain roots directly:
+            if (templateName.startsWith("@")) {
+                return ctx.createNegativeLookupResult();
+            }
+            
+            R lookupResult = ctx.lookupWithLocalizedThenAcquisitionStrategy(
+                    "@" + domain.name + "/" + templateName,
+                    ctx.getTemplateLocale());
+            if (lookupResult.isPositive()) {
+                return lookupResult;
+            }
+            
+            return ctx.lookupWithLocalizedThenAcquisitionStrategy("@default/" + templateName, ctx.getTemplateLocale());
+        }
+        
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateNameSpecialVariablesTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateNameSpecialVariablesTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateNameSpecialVariablesTest.java
new file mode 100644
index 0000000..7c9a98f
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateNameSpecialVariablesTest.java
@@ -0,0 +1,159 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.freemarker.core;
+
+import java.io.IOException;
+
+import org.apache.freemarker.test.TemplateTest;
+import org.apache.freemarker.test.TestConfigurationBuilder;
+import org.junit.Test;
+
+public class TemplateNameSpecialVariablesTest extends TemplateTest {
+    
+    private static final String PRINT_ALL_FTL
+            = "ct=${.currentTemplateName!'-'}, mt=${.mainTemplateName!'-'}";
+
+    @Test
+    public void testMainTemplateName() throws IOException, TemplateException {
+        addTemplateNameTestTemplates(".mainTemplateName");
+        assertOutputForNamed("main.ftl",
+                "In main: main.ftl\n"
+                + "In imp: main.ftl\n"
+                + "In main: main.ftl\n"
+                + "main.ftl\n"
+                + "{main.ftl}\n"
+                + "In imp call imp:\n"
+                + "main.ftl\n"
+                + "{main.ftl}\n"
+                + "After: main.ftl\n"
+                + "In main: main.ftl\n"
+                + "In inc: main.ftl\n"
+                + "In inc call imp:\n"
+                + "main.ftl\n"
+                + "{main.ftl}\n"
+                + "In main: main.ftl\n"
+                + "main.ftl\n"
+                + "{main.ftl}\n"
+                + "In inc call imp:\n"
+                + "main.ftl\n"
+                + "{main.ftl}\n"
+                + "In main: main.ftl\n");
+    }
+
+    @Test
+    public void testCurrentTemplateName() throws IOException, TemplateException {
+        addTemplateNameTestTemplates(".currentTemplateName");
+        assertOutputForNamed("main.ftl",
+                "In main: main.ftl\n"
+                + "In imp: imp.ftl\n"
+                + "In main: main.ftl\n"
+                + "imp.ftl\n"
+                + "{main.ftl}\n"
+                + "In imp call imp:\n"
+                + "imp.ftl\n"
+                + "{imp.ftl}\n"
+                + "After: imp.ftl\n"
+                + "In main: main.ftl\n"
+                + "In inc: inc.ftl\n"
+                + "In inc call imp:\n"
+                + "imp.ftl\n"
+                + "{inc.ftl}\n"
+                + "In main: main.ftl\n"
+                + "inc.ftl\n"
+                + "{main.ftl}\n"
+                + "In inc call imp:\n"
+                + "imp.ftl\n"
+                + "{inc.ftl}\n"
+                + "In main: main.ftl\n");
+    }
+
+    private void addTemplateNameTestTemplates(String specVar) {
+        addTemplate("main.ftl",
+                "In main: ${" + specVar + "}\n"
+                        + "<#import 'imp.ftl' as i>"
+                        + "In imp: ${inImp}\n"
+                        + "In main: ${" + specVar + "}\n"
+                        + "<@i.impM>${" + specVar + "}</@>\n"
+                        + "<@i.impM2 />\n"
+                        + "In main: ${" + specVar + "}\n"
+                        + "<#include 'inc.ftl'>"
+                        + "In main: ${" + specVar + "}\n"
+                        + "<@incM>${" + specVar + "}</@>\n"
+                        + "<@incM2 />\n"
+                        + "In main: ${" + specVar + "}\n"
+        );
+        addTemplate("imp.ftl",
+                "<#global inImp = " + specVar + ">"
+                        + "<#macro impM>"
+                        + "${" + specVar + "}\n"
+                        + "{<#nested>}"
+                        + "</#macro>"
+                        + "<#macro impM2>"
+                        + "In imp call imp:\n"
+                        + "<@impM>${" + specVar + "}</@>\n"
+                        + "After: ${" + specVar + "}"
+                        + "</#macro>"
+        );
+        addTemplate("inc.ftl",
+                "In inc: ${" + specVar + "}\n"
+                        + "In inc call imp:\n"
+                        + "<@i.impM>${" + specVar + "}</@>\n"
+                        + "<#macro incM>"
+                        + "${" + specVar + "}\n"
+                        + "{<#nested>}"
+                        + "</#macro>"
+                        + "<#macro incM2>"
+                        + "In inc call imp:\n"
+                        + "<@i.impM>${" + specVar + "}</@>"
+                        + "</#macro>"
+        );
+    }
+
+    @Test
+    public void testInAdhocTemplate() throws TemplateException, IOException {
+        addTemplate("inc.ftl", "Inc: " + PRINT_ALL_FTL);
+
+        // In nameless templates, the deprecated .templateName is "", but the new variables are missing values. 
+        assertOutput(new Template(null, PRINT_ALL_FTL + "; <#include 'inc.ftl'>", getConfiguration()),
+                "ct=-, mt=-; Inc: ct=inc.ftl, mt=-");
+        
+        assertOutput(new Template("foo.ftl", PRINT_ALL_FTL + "; <#include 'inc.ftl'>", getConfiguration()),
+                "ct=foo.ftl, mt=foo.ftl; Inc: ct=inc.ftl, mt=foo.ftl");
+    }
+
+    @Test
+    public void testInInterpretTemplate() throws TemplateException, IOException {
+        addToDataModel("t", PRINT_ALL_FTL);
+        assertOutput(new Template("foo.ftl", PRINT_ALL_FTL + "; <@t?interpret />", getConfiguration()),
+                "ct=foo.ftl, mt=foo.ftl; "
+                + "ct=foo.ftl->anonymous_interpreted, mt=foo.ftl");
+        assertOutput(new Template(null, PRINT_ALL_FTL + "; <@t?interpret />", getConfiguration()),
+                "ct=-, mt=-; "
+                + "ct=nameless_template->anonymous_interpreted, mt=-");
+        assertOutput(new Template("foo.ftl", PRINT_ALL_FTL + "; <@[t,'bar']?interpret />", getConfiguration()),
+                "ct=foo.ftl, mt=foo.ftl; "
+                + "ct=foo.ftl->bar, mt=foo.ftl");
+    }
+
+    @Override
+    protected Configuration createDefaultConfiguration() throws Exception {
+        return new TestConfigurationBuilder().whitespaceStripping(false).build();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateNotFoundMessageTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateNotFoundMessageTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateNotFoundMessageTest.java
new file mode 100644
index 0000000..66331ad
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/TemplateNotFoundMessageTest.java
@@ -0,0 +1,207 @@
+/*
+ * 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.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.Serializable;
+
+import org.apache.freemarker.core.templateresolver.MalformedTemplateNameException;
+import org.apache.freemarker.core.templateresolver.TemplateLoader;
+import org.apache.freemarker.core.templateresolver.TemplateLookupContext;
+import org.apache.freemarker.core.templateresolver.TemplateLookupResult;
+import org.apache.freemarker.core.templateresolver.TemplateLookupStrategy;
+import org.apache.freemarker.core.templateresolver.impl.ClassTemplateLoader;
+import org.apache.freemarker.core.templateresolver.impl.FileTemplateLoader;
+import org.apache.freemarker.core.templateresolver.impl.MultiTemplateLoader;
+import org.apache.freemarker.core.templateresolver.impl.StringTemplateLoader;
+import org.junit.Test;
+
+public class TemplateNotFoundMessageTest {
+
+    @Test
+    public void testFileTemplateLoader() throws IOException {
+        final File baseDir = new File(System.getProperty("user.home"));
+        final String errMsg = failWith(new FileTemplateLoader(baseDir));
+        showErrorMessage(errMsg);
+        assertThat(errMsg, containsString(baseDir.toString()));
+        assertThat(errMsg, containsString("FileTemplateLoader"));
+    }
+
+    @Test
+    public void testClassTemplateLoader() throws IOException {
+        final String errMsg = failWith(new ClassTemplateLoader(getClass(), "foo/bar"));
+        showErrorMessage(errMsg);
+        assertThat(errMsg, containsString("ClassTemplateLoader"));
+        assertThat(errMsg, containsString("foo/bar"));
+    }
+
+    @Test
+    public void testStringTemplateLoader() throws IOException {
+        StringTemplateLoader tl = new StringTemplateLoader();
+        tl.putTemplate("aaa", "A");
+        tl.putTemplate("bbb", "B");
+        tl.putTemplate("ccc", "C");
+        final String errMsg = failWith(tl);
+        showErrorMessage(errMsg);
+        assertThat(errMsg, containsString("StringTemplateLoader"));
+        assertThat(errMsg, containsString("aaa"));
+        assertThat(errMsg, containsString("bbb"));
+        assertThat(errMsg, containsString("ccc"));
+    }
+    
+    @Test
+    public void testMultiTemplateLoader() throws IOException {
+        final String errMsg = failWith(new MultiTemplateLoader(new TemplateLoader[] {
+                new StringTemplateLoader(),
+                new ClassTemplateLoader(getClass(), "foo/bar")
+        }));
+        showErrorMessage(errMsg);
+        assertThat(errMsg, containsString("MultiTemplateLoader"));
+        assertThat(errMsg, containsString("StringTemplateLoader"));
+        assertThat(errMsg, containsString("ClassTemplateLoader"));
+        assertThat(errMsg, containsString("foo/bar"));
+    }
+
+    @Test
+    public void testDefaultTemplateLoader() throws IOException {
+        String errMsg = failWith(null);
+        showErrorMessage(errMsg);
+        assertThat(errMsg, allOf(containsString("setTemplateLoader"), containsString("null")));
+    }
+    
+    @Test
+    public void testOtherMessageDetails() throws IOException {
+        // Non-null TemplateLoader:
+        StringTemplateLoader emptyLoader = new StringTemplateLoader();
+        {
+            String errMsg = failWith(emptyLoader, "../x");
+            showErrorMessage(errMsg);
+            assertThat(errMsg,
+                    allOf(
+                            containsStringIgnoringCase("Malformed template name"),
+                            containsStringIgnoringCase("root directory")));
+        }
+        {
+            String errMsg = failWith(emptyLoader, "x\u0000y");
+            showErrorMessage(errMsg);
+            assertThat(errMsg,
+                    allOf(
+                            containsStringIgnoringCase("Malformed template name"),
+                            containsStringIgnoringCase("null character")));
+        }
+        {
+            String errMsg = failWith(emptyLoader, "x\\y");
+            showErrorMessage(errMsg);
+            assertThat(errMsg,
+                    allOf(containsStringIgnoringCase("warning"), containsStringIgnoringCase("backslash")));
+        }
+        {
+            String errMsg = failWith(emptyLoader, "x/./y");
+            showErrorMessage(errMsg);
+            assertThat(errMsg,
+                    allOf(containsStringIgnoringCase("normalized"), containsStringIgnoringCase("x/y")));
+        }
+        {
+            String errMsg = failWith(emptyLoader, "/x/y");
+            showErrorMessage(errMsg);
+            assertThat(errMsg, not(containsStringIgnoringCase("normalized")));
+        }
+        {
+            String errMsg = failWith(emptyLoader, "x/y");
+            showErrorMessage(errMsg);
+            assertThat(errMsg, not(containsStringIgnoringCase("normalized")));
+            assertThat(errMsg, not(containsStringIgnoringCase("lookup strategy")));
+        }
+
+        Configuration.Builder cfgB = new Configuration.Builder(Configuration.VERSION_3_0_0)
+                .templateLoader(new StringTemplateLoader())
+                .templateLookupStrategy(
+                        new TemplateLookupStrategy() {
+                            @Override
+                            public TemplateLookupResult lookup(TemplateLookupContext ctx) throws IOException {
+                                return ctx.lookupWithAcquisitionStrategy(ctx.getTemplateName());
+                            }
+                        }
+        );
+        {
+            String errMsg = failWith(emptyLoader, "x/y",
+                    new TemplateLookupStrategy() {
+                        @Override
+                        public TemplateLookupResult lookup(TemplateLookupContext ctx) throws IOException {
+                            return ctx.lookupWithAcquisitionStrategy(ctx.getTemplateName());
+                        }
+                    }
+            );
+            showErrorMessage(errMsg);
+            assertThat(errMsg, containsStringIgnoringCase("lookup strategy"));
+        }
+        
+        try {
+            cfgB.build().getTemplate("./missing", null, new DomainLookupCondition());
+            fail();
+        } catch (TemplateNotFoundException e) {
+            showErrorMessage(e.getMessage());
+            assertThat(e.getMessage(), containsStringIgnoringCase("example.com"));
+        }
+    }
+
+    private void showErrorMessage(String errMsg) {
+        // System.out.println(errMsg);
+    }
+
+    private String failWith(TemplateLoader tl, String name, TemplateLookupStrategy templateLookupStrategy) {
+        try {
+            Configuration.Builder cfgB = new Configuration.Builder(Configuration.VERSION_3_0_0);
+            cfgB.setTemplateLoader(tl);
+            if (templateLookupStrategy != null) {
+                cfgB.setTemplateLookupStrategy(templateLookupStrategy);
+            }
+            cfgB.build().getTemplate(name);
+            fail();
+        } catch (TemplateNotFoundException | MalformedTemplateNameException e) {
+            return e.getMessage();
+        } catch (IOException e) {
+            fail("Unexpected exception: " + e);
+        }
+        return null;
+    }
+
+    private String failWith(TemplateLoader tl, String name) {
+        return failWith(tl, name, null);
+    }
+
+    private String failWith(TemplateLoader tl) {
+        return failWith(tl, "missing.ftl", null);
+    }
+
+    @SuppressWarnings("serial")
+    private static final class DomainLookupCondition implements Serializable {
+        @Override
+        public String toString() {
+            return "example.com";
+        }
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/TheadInterruptingSupportTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/TheadInterruptingSupportTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/TheadInterruptingSupportTest.java
new file mode 100644
index 0000000..a0174b3
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/TheadInterruptingSupportTest.java
@@ -0,0 +1,163 @@
+/*
+ * 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 java.util.Map;
+
+import org.apache.freemarker.core.ThreadInterruptionSupportTemplatePostProcessor.TemplateProcessingThreadInterruptedException;
+import org.apache.freemarker.core.model.TemplateDirectiveBody;
+import org.apache.freemarker.core.model.TemplateDirectiveModel;
+import org.apache.freemarker.core.model.TemplateModel;
+import org.apache.freemarker.core.util._NullWriter;
+import org.apache.freemarker.test.TestConfigurationBuilder;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class TheadInterruptingSupportTest {
+    
+    private static final Logger LOG = LoggerFactory.getLogger(TheadInterruptingSupportTest.class);
+
+    private static final int TEMPLATE_INTERRUPTION_TIMEOUT = 5000;
+    private final Configuration cfg = new TestConfigurationBuilder().build();
+
+    @Test
+    public void test() throws IOException, InterruptedException {
+        assertCanBeInterrupted("<#list 1.. as x></#list>");
+        assertCanBeInterrupted("<#list 1.. as x>${x}</#list>");
+        assertCanBeInterrupted("<#list 1.. as x>t${x}</#list>");
+        assertCanBeInterrupted("<#list 1.. as x><#list 1.. as y>${y}</#list></#list>");
+        assertCanBeInterrupted("<#list 1.. as x>${x}<#else>nope</#list>");
+        assertCanBeInterrupted("<#list 1..>[<#items as x>${x}</#items>]<#else>nope</#list>");
+        assertCanBeInterrupted("<@customLoopDirective />");
+        assertCanBeInterrupted("<@customLoopDirective>x</@>");
+        assertCanBeInterrupted("<@customLoopDirective><#if true>x</#if></@>");
+        assertCanBeInterrupted("<#macro selfCalling><@sleepDirective/><@selfCalling /></#macro><@selfCalling />");
+        assertCanBeInterrupted("<#function selfCalling><@sleepDirective/>${selfCalling()}</#function>${selfCalling()}");
+        assertCanBeInterrupted("<#list 1.. as _><#attempt><@sleepDirective/><#recover>suppress</#attempt></#list>");
+        assertCanBeInterrupted("<#attempt><#list 1.. as _></#list><#recover>suppress</#attempt>");
+    }
+
+    private void assertCanBeInterrupted(final String templateSourceCode) throws IOException, InterruptedException {
+        TemplateRunnerThread trt = new TemplateRunnerThread(templateSourceCode);
+        trt.start();
+        synchronized (trt) {
+            while (!trt.isStarted()) {
+                trt.wait();
+            }
+        }
+        Thread.sleep(50); // Just to ensure (hope...) that the template execution reaches "deep" enough
+        trt.interrupt();
+        trt.join(TEMPLATE_INTERRUPTION_TIMEOUT);
+        assertTrue(trt.isTemplateProcessingInterrupted());
+    }
+
+    public class TemplateRunnerThread extends Thread {
+
+        private final Template template;
+        private boolean started;
+        private boolean templateProcessingInterrupted;
+
+        public TemplateRunnerThread(String templateSourceCode) throws IOException {
+            template = new Template(null, "<@startedDirective/>" + templateSourceCode, cfg);
+            _CoreAPI.addThreadInterruptedChecks(template);
+        }
+
+        @Override
+        public void run() {
+            try {
+                template.process(this, _NullWriter.INSTANCE);
+            } catch (TemplateProcessingThreadInterruptedException e) {
+                //LOG.debug("Template processing interrupted", e);
+                synchronized (this) {
+                    templateProcessingInterrupted = true;
+                }
+            } catch (Throwable e) {
+                LOG.error("Template processing failed", e);
+            }
+        }
+
+        public synchronized boolean isTemplateProcessingInterrupted() {
+            return templateProcessingInterrupted;
+        }
+        
+        public synchronized boolean isStarted() {
+            return started;
+        }
+        
+        public TemplateDirectiveModel getStartedDirective() {
+            return new StartedDirective();
+        }
+
+        public TemplateDirectiveModel getCustomLoopDirective() {
+            return new CustomLoopDirective();
+        }
+        
+        public TemplateDirectiveModel getSleepDirective() {
+            return new SleepDirective();
+        }
+
+        public class StartedDirective implements TemplateDirectiveModel {
+            
+            @Override
+            public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body)
+                    throws TemplateException, IOException {
+                synchronized (TemplateRunnerThread.this) {
+                    started = true;
+                    TemplateRunnerThread.this.notifyAll();
+                }
+            }
+            
+        }
+
+        public class CustomLoopDirective implements TemplateDirectiveModel {
+
+            @Override
+            public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body)
+                    throws TemplateException, IOException {
+                // Deliberate infinite loop
+                while (true) {
+                    body.render(_NullWriter.INSTANCE);
+                }
+            }
+            
+        }
+        
+        public class SleepDirective implements TemplateDirectiveModel {
+
+            @Override
+            public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body)
+                    throws TemplateException, IOException {
+                try {
+                    Thread.sleep(100);
+                } catch (InterruptedException e) {
+                    // Thread.sleep has reset the interrupted flag (because it has thrown InterruptedException).  
+                    Thread.currentThread().interrupt();
+                }
+            }
+            
+        }
+        
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/TypeErrorMessagesTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/TypeErrorMessagesTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/TypeErrorMessagesTest.java
new file mode 100644
index 0000000..749d057
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/TypeErrorMessagesTest.java
@@ -0,0 +1,105 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import java.io.StringReader;
+import java.util.Map;
+
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+
+import org.apache.freemarker.test.TemplateTest;
+import org.junit.Test;
+import org.w3c.dom.Document;
+import org.xml.sax.InputSource;
+
+public class TypeErrorMessagesTest extends TemplateTest {
+
+    static final Document doc;
+    static {
+        try {
+            DocumentBuilder docBuilder;
+            docBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
+            doc = docBuilder.parse(new InputSource(new StringReader(
+                    "<a><b>123</b><c a='true'>1</c><c a='false'>2</c></a>")));
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to build data-model", e);
+        }
+    }
+
+    @Test
+    public void testNumericalBinaryOperator() {
+        assertErrorContains("${n - s}", "\"-\"", "right-hand", "number", "string");
+        assertErrorContains("${s - n}", "\"-\"", "left-hand", "number", "string");
+    }
+
+    @Test
+    public void testGetterMistake() {
+        assertErrorContains("${bean.getX}", "${...}",
+                "number", "string", "method", "obj.getSomething", "obj.something");
+        assertErrorContains("${1 * bean.getX}", "right-hand",
+                "number", "\\!string", "method", "obj.getSomething", "obj.something");
+        assertErrorContains("<#if bean.isB></#if>", "condition",
+                "boolean", "method", "obj.isSomething", "obj.something");
+        assertErrorContains("<#if bean.isB></#if>", "condition",
+                "boolean", "method", "obj.isSomething", "obj.something");
+        assertErrorContains("${bean.voidM}",
+                "string", "method", "\\!()");
+        assertErrorContains("${bean.intM}",
+                "string", "method", "obj.something()");
+        assertErrorContains("${bean.intMP}",
+                "string", "method", "obj.something(params)");
+    }
+
+    @Test
+    public void testXMLTypeMismarches() throws Exception {
+        assertErrorContains("${doc.a.c}",
+                "used as string", "query result", "2", "multiple matches");
+        assertErrorContains("${doc.a.c?boolean}",
+                "used as string", "query result", "2", "multiple matches");
+        assertErrorContains("${doc.a.d}",
+                "used as string", "query result", "0", "no matches");
+        assertErrorContains("${doc.a.d?boolean}",
+                "used as string", "query result", "0", "no matches");
+        
+        assertErrorContains("${doc.a.c.@a}",
+                "used as string", "query result", "2", "multiple matches");
+        assertErrorContains("${doc.a.d.@b}",
+                "used as string", "query result", "x", "no matches");
+        
+        assertErrorContains("${doc.a.b * 2}",
+                "used as number", "text", "explicit conversion");
+        assertErrorContains("<#if doc.a.b></#if>",
+                "used as number", "text", "explicit conversion");
+
+        assertErrorContains("${doc.a.d?nodeName}",
+                "used as node", "query result", "0", "no matches");
+        assertErrorContains("${doc.a.c?nodeName}",
+                "used as node", "query result", "2", "multiple matches");
+    }
+
+    @Override
+    protected Object createDataModel() {
+        Map<String, Object> dataModel = createCommonTestValuesDataModel();
+        dataModel.put("doc", doc);
+        return dataModel;
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/UnclosedCommentTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/UnclosedCommentTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/UnclosedCommentTest.java
new file mode 100644
index 0000000..99bf5d3
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/UnclosedCommentTest.java
@@ -0,0 +1,41 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import java.io.IOException;
+
+import org.apache.freemarker.test.TemplateTest;
+import org.junit.Test;
+
+public class UnclosedCommentTest extends TemplateTest {
+    
+    @Test
+    public void test() throws IOException, TemplateException {
+        assertErrorContains("foo<#--", "end of file");  // Not too good...
+        assertErrorContains("foo<#-- ", "Unclosed", "<#--");
+        assertErrorContains("foo<#--bar", "Unclosed", "<#--");
+        assertErrorContains("foo\n<#--\n", "Unclosed", "<#--");
+        assertErrorContains("foo<#noparse>", "end of file");  // Not too good...
+        assertErrorContains("foo<#noparse> ", "Unclosed", "#noparse");
+        assertErrorContains("foo<#noparse>bar", "Unclosed", "#noparse");
+        assertErrorContains("foo\n<#noparse>\n", "Unclosed", "#noparse");
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/VersionTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/VersionTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/VersionTest.java
new file mode 100644
index 0000000..8c54df5
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/VersionTest.java
@@ -0,0 +1,227 @@
+/*
+ * 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.Date;
+
+import org.junit.Test;
+
+@SuppressWarnings("boxing")
+public class VersionTest {
+
+    @Test
+    public void testFromNumber() {
+        Version v = new Version(1, 2, 3);
+        assertEquals("1.2.3", v.toString());
+        assertEquals(1002003, v.intValue());
+        assertEquals(1, v.getMajor());
+        assertEquals(2, v.getMinor());
+        assertEquals(3, v.getMicro());
+        assertNull(v.getExtraInfo());
+        assertNull(v.isGAECompliant());
+        assertNull(v.getBuildDate());
+    }
+
+    @Test
+    public void testFromNumber2() {
+        Version v = new Version(1, 2, 3, "beta8", Boolean.TRUE, new Date(5000));
+        assertEquals("1.2.3-beta8", v.toString());
+        assertEquals("beta8", v.getExtraInfo());
+        assertTrue(v.isGAECompliant().booleanValue());
+        assertEquals(new Date(5000), v.getBuildDate());
+    }
+
+    @Test
+    public void testFromNumber3() {
+        Version v = new Version(new Version(1, 2, 3).intValue());
+        assertEquals(1, v.getMajor());
+        assertEquals(2, v.getMinor());
+        assertEquals(3, v.getMicro());
+    }
+    
+    public void testFromNumberIncubating() {
+        Version v = new Version(2, 3, 24, "rc01-incubating", Boolean.FALSE, new Date(5000));
+        assertEquals("2.3.24-rc01-incubating", v.toString());
+        assertEquals("rc01-incubating", v.getExtraInfo());
+        assertFalse(v.isGAECompliant().booleanValue());
+        assertEquals(new Date(5000), v.getBuildDate());
+    }
+    
+    @Test
+    public void testFromString() {
+        Version v = new Version("1.2.3-beta2");
+        assertEquals("1.2.3-beta2", v.toString());
+        assertEquals(1002003, v.intValue());
+        assertEquals(1, v.getMajor());
+        assertEquals(2, v.getMinor());
+        assertEquals(3, v.getMicro());
+        assertEquals("beta2", v.getExtraInfo());
+        assertNull(v.isGAECompliant());
+        assertNull(v.getBuildDate());
+    }
+
+    @Test
+    public void testFromString2() {
+        Version v = new Version("10.20.30", Boolean.TRUE, new Date(5000));
+        assertEquals("10.20.30", v.toString());
+        assertEquals(10020030, v.intValue());
+        assertEquals(10, v.getMajor());
+        assertEquals(20, v.getMinor());
+        assertEquals(30, v.getMicro());
+        assertNull(v.getExtraInfo());
+        assertTrue(v.isGAECompliant().booleanValue());
+        assertEquals(new Date(5000), v.getBuildDate());
+    }
+
+    @Test
+    public void testFromString3() {
+        Version v = new Version("01.002.0003-20130524");
+        assertEquals("01.002.0003-20130524", v.toString());
+        assertEquals(1, v.getMajor());
+        assertEquals(2, v.getMinor());
+        assertEquals(3, v.getMicro());
+        assertEquals("20130524", v.getExtraInfo());
+
+        v = new Version("01.002.0003.4");
+        assertEquals("01.002.0003.4", v.toString());
+        assertEquals(1, v.getMajor());
+        assertEquals(2, v.getMinor());
+        assertEquals(3, v.getMicro());
+        assertEquals("4", v.getExtraInfo());
+        
+        v = new Version("1.2.3.FC");
+        assertEquals("1.2.3.FC", v.toString());
+        assertEquals("FC", v.getExtraInfo());
+        
+        v = new Version("1.2.3mod");
+        assertEquals("1.2.3mod", v.toString());
+        assertEquals(1, v.getMajor());
+        assertEquals(2, v.getMinor());
+        assertEquals(3, v.getMicro());
+        assertEquals("mod", v.getExtraInfo());
+        
+    }
+
+    @Test
+    public void testFromStringIncubating() {
+        Version v = new Version("2.3.24-rc01-incubating");
+        assertEquals("2.3.24-rc01-incubating", v.toString());
+        assertEquals(2, v.getMajor());
+        assertEquals(3, v.getMinor());
+        assertEquals(24, v.getMicro());
+        assertEquals("rc01-incubating", v.getExtraInfo());
+    }
+    
+    @Test
+    public void testHashAndEquals() {
+        Version v1 = new Version("1.2.3-beta2");
+        Version v2 = new Version(1, 2, 3, "beta2", null, null);
+        assertEquals(v1, v2);
+        assertEquals(v1.hashCode(), v2.hashCode());
+        
+        v2 = new Version("1.2.3-beta3");
+        assertTrue(!v1.equals(v2));
+        assertTrue(v1.hashCode() != v2.hashCode());
+        
+        v2 = new Version(1, 2, 3, "beta2", true, null);
+        assertTrue(!v1.equals(v2));
+        assertTrue(v1.hashCode() != v2.hashCode());
+        
+        v2 = new Version(1, 2, 3, "beta2", null, new Date(5000));
+        assertTrue(!v1.equals(v2));
+        assertTrue(v1.hashCode() != v2.hashCode());
+        
+        v2 = new Version("1.2.9-beta2");
+        assertTrue(!v1.equals(v2));
+        assertTrue(v1.hashCode() != v2.hashCode());
+        
+        v2 = new Version("1.9.3-beta2");
+        assertTrue(!v1.equals(v2));
+        assertTrue(v1.hashCode() != v2.hashCode());
+        
+        v2 = new Version("9.2.3-beta2");
+        assertTrue(!v1.equals(v2));
+        assertTrue(v1.hashCode() != v2.hashCode());
+        
+        v2 = new Version("1.2.3");
+        assertTrue(!v1.equals(v2));
+        assertTrue(v1.hashCode() != v2.hashCode());
+    }
+
+    @Test
+    public void testShortForms() {
+        Version v = new Version("1.0.0-beta2");
+        assertEquals(v, new Version("1.0-beta2"));
+        assertEquals(v, new Version("1-beta2"));
+
+        v = new Version("1.0.0");
+        assertEquals(v, new Version("1.0"));
+        assertEquals(v, new Version("1"));
+    }
+    
+    @Test
+    public void testMalformed() {
+        try {
+            new Version("1.2.");
+            fail();
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+        
+        try {
+            new Version("1.2.3.");
+            fail();
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+        
+        try {
+            new Version("1..3");
+            fail();
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+
+        try {
+            new Version(".2");
+            fail();
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+        
+        try {
+            new Version("a");
+            fail();
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+        
+        try {
+            new Version("-a");
+            fail();
+        } catch (IllegalArgumentException e) {
+            // expected
+        }
+    }
+    
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/WhitespaceStrippingTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/WhitespaceStrippingTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/WhitespaceStrippingTest.java
new file mode 100644
index 0000000..8e5b52e
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/WhitespaceStrippingTest.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import java.io.IOException;
+
+import org.apache.freemarker.test.TemplateTest;
+import org.apache.freemarker.test.TestConfigurationBuilder;
+import org.junit.Test;
+
+public class WhitespaceStrippingTest extends TemplateTest {
+
+    @Test
+    public void testBasics() throws Exception {
+        assertOutput("<#assign x = 1>\n<#assign y = 2>\n${x}\n${y}", "1\n2", "\n\n1\n2");
+        assertOutput(" <#assign x = 1> \n <#assign y = 2> \n${x}\n${y}", "1\n2", "  \n  \n1\n2");
+    }
+
+    @Test
+    public void testFTLHeader() throws Exception {
+        assertOutput("<#ftl>x", "x", "x");
+        assertOutput("  <#ftl>  x", "  x", "  x");
+        assertOutput("\n<#ftl>\nx", "x", "x");
+        assertOutput("\n<#ftl>\t \nx", "x", "x");
+        assertOutput("  \n \n  <#ftl> \n \n  x", " \n  x", " \n  x");
+    }
+
+    @Test
+    public void testComment() throws Exception {
+        assertOutput(" a <#-- --> b ", " a  b ", " a  b ");
+        assertOutput(" a \n<#-- -->\n b ", " a \n b ", " a \n\n b ");
+        // These are wrong, but needed for 2.3.0 compatibility:
+        assertOutput(" a \n <#-- --> \n b ", " a \n  b ", " a \n  \n b ");
+        assertOutput(" a \n\t<#-- --> \n b ", " a \n\t b ", " a \n\t \n b ");
+    }
+    
+    private void assertOutput(String ftl, String expectedOutStripped, String expectedOutNonStripped)
+            throws IOException, TemplateException {
+        setConfiguration(new TestConfigurationBuilder().build());
+        assertOutput(ftl, expectedOutStripped);
+        
+        setConfiguration(new TestConfigurationBuilder().whitespaceStripping(false).build());
+        assertOutput(ftl, expectedOutNonStripped);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/XHTMLOutputFormatTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/XHTMLOutputFormatTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/XHTMLOutputFormatTest.java
new file mode 100644
index 0000000..77951ed
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/XHTMLOutputFormatTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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.outputformat.impl.XHTMLOutputFormat.*;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.StringWriter;
+
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.junit.Test; 
+
+public class XHTMLOutputFormatTest {
+    
+    @Test
+    public void testOutputMO() throws TemplateModelException, IOException {
+       StringWriter out = new StringWriter();
+       INSTANCE.output(INSTANCE.fromPlainTextByEscaping("a'b"), out);
+       assertEquals("a&#39;b", out.toString());
+    }
+    
+    @Test
+    public void testOutputString() throws TemplateModelException, IOException {
+        StringWriter out = new StringWriter();
+        INSTANCE.output("a'b", out);
+        assertEquals("a&#39;b", out.toString());
+    }
+    
+    @Test
+    public void testEscaplePlainText() {
+        assertEquals("", INSTANCE.escapePlainText(""));
+        assertEquals("a", INSTANCE.escapePlainText("a"));
+        assertEquals("&lt;a&amp;b&#39;c&quot;d&gt;", INSTANCE.escapePlainText("<a&b'c\"d>"));
+        assertEquals("&lt;&gt;", INSTANCE.escapePlainText("<>"));
+    }
+    
+    @Test
+    public void testGetMimeType() {
+        assertEquals("application/xhtml+xml", INSTANCE.getMimeType());
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/XMLOutputFormatTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/XMLOutputFormatTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/XMLOutputFormatTest.java
new file mode 100644
index 0000000..7e95e6d
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/XMLOutputFormatTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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.outputformat.impl.XMLOutputFormat.*;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.io.StringWriter;
+
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.junit.Test; 
+
+public class XMLOutputFormatTest {
+    
+    @Test
+    public void testOutputMO() throws TemplateModelException, IOException {
+       StringWriter out = new StringWriter();
+       INSTANCE.output(INSTANCE.fromPlainTextByEscaping("a'b"), out);
+       assertEquals("a&apos;b", out.toString());
+    }
+    
+    @Test
+    public void testOutputString() throws TemplateModelException, IOException {
+        StringWriter out = new StringWriter();
+        INSTANCE.output("a'b", out);
+        assertEquals("a&apos;b", out.toString());
+    }
+    
+    @Test
+    public void testEscaplePlainText() {
+        assertEquals("", INSTANCE.escapePlainText(""));
+        assertEquals("a", INSTANCE.escapePlainText("a"));
+        assertEquals("&lt;a&amp;b&apos;c&quot;d&gt;", INSTANCE.escapePlainText("<a&b'c\"d>"));
+        assertEquals("&lt;&gt;", INSTANCE.escapePlainText("<>"));
+    }
+    
+    @Test
+    public void testGetMimeType() {
+        assertEquals("application/xml", INSTANCE.getMimeType());
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/AbstractParallelIntrospectionTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/AbstractParallelIntrospectionTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/AbstractParallelIntrospectionTest.java
new file mode 100644
index 0000000..bdb9a56
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/AbstractParallelIntrospectionTest.java
@@ -0,0 +1,126 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core.model.impl;
+
+import org.apache.freemarker.core.Configuration;
+import org.apache.freemarker.core.model.TemplateHashModel;
+import org.apache.freemarker.core.model.TemplateMethodModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateNumberModel;
+
+import junit.framework.TestCase;
+
+public abstract class AbstractParallelIntrospectionTest extends TestCase {
+    
+    private static final int NUM_THREADS = 8;
+    private static final int NUM_ENTITYES = 8;
+    private static final int NUM_MEMBERS = 8;
+    private static final int ITERATIONS = 20000;
+    private static final double CACHE_CLEARING_CHANCE = 0.01;
+    
+    private DefaultObjectWrapper ow = new DefaultObjectWrapper.Builder(Configuration.VERSION_3_0_0)
+            .usePrivateCaches(true).build();
+    
+    public AbstractParallelIntrospectionTest(String name) {
+        super(name);
+    }
+    
+    public void testReliability() {
+        testReliability(ITERATIONS);
+    }
+    
+    public void testReliability(int iterations) {
+        TestThread[] ts = new TestThread[NUM_THREADS]; 
+        for (int i = 0; i < NUM_THREADS; i++) {
+            ts[i] = new TestThread(iterations);
+            ts[i].start();
+        }
+
+        for (int i = 0; i < NUM_THREADS; i++) {
+            try {
+                ts[i].join();
+                if (ts[i].error != null) {
+                    throw new AssertionError(ts[i].error);
+                }
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+        }
+    }
+
+    protected abstract TemplateHashModel getWrappedEntity(int objIdx) throws TemplateModelException;
+    
+    protected final DefaultObjectWrapper getObjectWrapper() {
+        return ow;
+    }
+    
+    private class TestThread extends Thread {
+        
+        private final int iterations;
+        
+        private Throwable error;
+        
+        private TestThread(int iterations) {
+            this.iterations = iterations;
+        }
+
+        @Override
+        public void run() {
+            try {
+                for (int i = 0; i < iterations; i++) {
+                    if (Math.random() < CACHE_CLEARING_CHANCE) {
+                        ow.clearClassIntrospecitonCache();
+                    }
+                    int objIdx = (int) (Math.random() * NUM_ENTITYES);
+                    TemplateHashModel h = getWrappedEntity(objIdx);
+                    int mIdx = (int) (Math.random() * NUM_MEMBERS);
+                    testProperty(h, objIdx, mIdx);
+                    testMethod(h, objIdx, mIdx);
+                }
+            } catch (Throwable e) {
+                error = e;
+            }
+        }
+
+        private void testProperty(TemplateHashModel h, int objIdx, int mIdx)
+                throws TemplateModelException, AssertionError {
+            TemplateNumberModel pv = (TemplateNumberModel) h.get("p" + mIdx);
+            final int expected = objIdx * 1000 + mIdx;
+            final int got = pv.getAsNumber().intValue();
+            if (got != expected) {
+                throw new AssertionError("Property assertation failed; " +
+                        "expected " + expected + ", but got " + got);
+            }
+        }
+
+        private void testMethod(TemplateHashModel h, int objIdx, int mIdx)
+                throws TemplateModelException, AssertionError {
+            TemplateMethodModel pv = (TemplateMethodModel) h.get("m" + mIdx);
+            final int expected = objIdx * 1000 + mIdx;
+            final int got = ((TemplateNumberModel) pv.exec(null)).getAsNumber().intValue();
+            if (got != expected) {
+                throw new AssertionError("Method assertation failed; " +
+                        "expected " + expected + ", but got " + got);
+            }
+        }
+        
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/AlphabeticalMethodSorter.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/AlphabeticalMethodSorter.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/AlphabeticalMethodSorter.java
new file mode 100644
index 0000000..149ad0d
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/model/impl/AlphabeticalMethodSorter.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core.model.impl;
+
+import java.beans.MethodDescriptor;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+class AlphabeticalMethodSorter implements MethodSorter {
+
+    private final boolean desc;
+    
+    public AlphabeticalMethodSorter(boolean desc) {
+        this.desc = desc;
+    }
+
+    @Override
+    public void sortMethodDescriptors(List<MethodDescriptor> methodDescriptors) {
+        Collections.sort(methodDescriptors, new Comparator<MethodDescriptor>() {
+            public int compare(MethodDescriptor o1, MethodDescriptor o2) {
+                int res = o1.getMethod().toString().compareTo(o2.getMethod().toString());
+                return desc ? -res : res;
+            }
+        });
+    }
+    
+}
\ No newline at end of file