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

[47/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/DateFormatTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/DateFormatTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/DateFormatTest.java
new file mode 100644
index 0000000..4ad5937
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/DateFormatTest.java
@@ -0,0 +1,464 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.freemarker.core;
+
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.sql.Time;
+import java.sql.Timestamp;
+import java.util.Collections;
+import java.util.Date;
+import java.util.Locale;
+import java.util.TimeZone;
+
+import org.apache.freemarker.core.model.TemplateDateModel;
+import org.apache.freemarker.core.model.TemplateModelException;
+import org.apache.freemarker.core.model.impl.SimpleDate;
+import org.apache.freemarker.core.templateresolver.ConditionalTemplateConfigurationFactory;
+import org.apache.freemarker.core.templateresolver.FileNameGlobMatcher;
+import org.apache.freemarker.core.userpkg.AppMetaTemplateDateFormatFactory;
+import org.apache.freemarker.core.userpkg.EpochMillisDivTemplateDateFormatFactory;
+import org.apache.freemarker.core.userpkg.EpochMillisTemplateDateFormatFactory;
+import org.apache.freemarker.core.userpkg.HTMLISOTemplateDateFormatFactory;
+import org.apache.freemarker.core.userpkg.LocAndTZSensitiveTemplateDateFormatFactory;
+import org.apache.freemarker.core.valueformat.TemplateDateFormat;
+import org.apache.freemarker.core.valueformat.TemplateDateFormatFactory;
+import org.apache.freemarker.core.valueformat.UndefinedCustomFormatException;
+import org.apache.freemarker.core.valueformat.impl.AliasTemplateDateFormatFactory;
+import org.apache.freemarker.test.TemplateTest;
+import org.apache.freemarker.test.TestConfigurationBuilder;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableMap;
+
+public class DateFormatTest extends TemplateTest {
+    
+    /** 2015-09-06T12:00:00Z */
+    private static long T = 1441540800000L;
+    private static TemplateDateModel TM = new SimpleDate(new Date(T), TemplateDateModel.DATETIME);
+    
+    private TestConfigurationBuilder createConfigurationBuilder() {
+        return new TestConfigurationBuilder()
+                .locale(Locale.US)
+                .timeZone(TimeZone.getTimeZone("GMT+01:00"))
+                .sqlDateAndTimeTimeZone(TimeZone.getTimeZone("UTC"))
+                .customDateFormats(ImmutableMap.of(
+                        "epoch", EpochMillisTemplateDateFormatFactory.INSTANCE,
+                        "loc", LocAndTZSensitiveTemplateDateFormatFactory.INSTANCE,
+                        "div", EpochMillisDivTemplateDateFormatFactory.INSTANCE,
+                        "appMeta", AppMetaTemplateDateFormatFactory.INSTANCE,
+                        "htmlIso", HTMLISOTemplateDateFormatFactory.INSTANCE));
+    }
+
+    @Override
+    protected Configuration createDefaultConfiguration() throws Exception {
+        return createConfigurationBuilder().build();
+    }
+
+    @Test
+    public void testCustomFormat() throws Exception {
+        addToDataModel("d", new Date(123456789));
+        assertOutput(
+                "${d?string.@epoch} ${d?string.@epoch} <#setting locale='de_DE'>${d?string.@epoch}",
+                "123456789 123456789 123456789");
+
+        setConfigurationWithDateTimeFormat("@epoch");
+        assertOutput(
+                "<#assign d = d?datetime>"
+                + "${d} ${d?string} <#setting locale='de_DE'>${d}",
+                "123456789 123456789 123456789");
+
+        setConfigurationWithDateTimeFormat("@htmlIso");
+        assertOutput(
+                "<#assign d = d?datetime>"
+                + "${d} ${d?string} <#setting locale='de_DE'>${d}",
+                "1970-01-02<span class='T'>T</span>10:17:36Z "
+                + "1970-01-02T10:17:36Z "
+                + "1970-01-02<span class='T'>T</span>10:17:36Z");
+    }
+
+    @Test
+    public void testLocaleChange() throws Exception {
+        addToDataModel("d", new Date(123456789));
+        assertOutput(
+                "${d?string.@loc} ${d?string.@loc} "
+                + "<#setting locale='de_DE'>"
+                + "${d?string.@loc} ${d?string.@loc} "
+                + "<#setting locale='en_US'>"
+                + "${d?string.@loc} ${d?string.@loc}",
+                "123456789@en_US:GMT+01:00 123456789@en_US:GMT+01:00 "
+                + "123456789@de_DE:GMT+01:00 123456789@de_DE:GMT+01:00 "
+                + "123456789@en_US:GMT+01:00 123456789@en_US:GMT+01:00");
+
+        setConfigurationWithDateTimeFormat("@loc");
+        assertOutput(
+                "<#assign d = d?datetime>"
+                + "${d} ${d?string} "
+                + "<#setting locale='de_DE'>"
+                + "${d} ${d?string} "
+                + "<#setting locale='en_US'>"
+                + "${d} ${d?string}",
+                "123456789@en_US:GMT+01:00 123456789@en_US:GMT+01:00 "
+                + "123456789@de_DE:GMT+01:00 123456789@de_DE:GMT+01:00 "
+                + "123456789@en_US:GMT+01:00 123456789@en_US:GMT+01:00");
+    }
+
+    @Test
+    public void testTimeZoneChange() throws Exception {
+        addToDataModel("d", new Date(123456789));
+        setConfigurationWithDateTimeFormat("iso");
+        assertOutput(
+                "${d?string.@loc} ${d?string.@loc} ${d?datetime?isoLocal} "
+                + "<#setting timeZone='GMT+02:00'>"
+                + "${d?string.@loc} ${d?string.@loc} ${d?datetime?isoLocal} "
+                + "<#setting timeZone='GMT+01:00'>"
+                + "${d?string.@loc} ${d?string.@loc} ${d?datetime?isoLocal}",
+                "123456789@en_US:GMT+01:00 123456789@en_US:GMT+01:00 1970-01-02T11:17:36+01:00 "
+                + "123456789@en_US:GMT+02:00 123456789@en_US:GMT+02:00 1970-01-02T12:17:36+02:00 "
+                + "123456789@en_US:GMT+01:00 123456789@en_US:GMT+01:00 1970-01-02T11:17:36+01:00");
+
+        setConfigurationWithDateTimeFormat("@loc");
+        assertOutput(
+                "<#assign d = d?datetime>"
+                + "${d} ${d?string} "
+                + "<#setting timeZone='GMT+02:00'>"
+                + "${d} ${d?string} "
+                + "<#setting timeZone='GMT+01:00'>"
+                + "${d} ${d?string}",
+                "123456789@en_US:GMT+01:00 123456789@en_US:GMT+01:00 "
+                + "123456789@en_US:GMT+02:00 123456789@en_US:GMT+02:00 "
+                + "123456789@en_US:GMT+01:00 123456789@en_US:GMT+01:00");
+    }
+    
+    @Test
+    public void testWrongFormatStrings() throws Exception {
+        setConfigurationWithDateTimeFormat("x1");
+        assertErrorContains("${.now}", "\"x1\"", "'x'");
+        assertErrorContains("${.now?string}", "\"x1\"", "'x'");
+        setConfigurationWithDateTimeFormat("short");
+        assertErrorContains("${.now?string('x2')}", "\"x2\"", "'x'");
+        assertErrorContains("${.now?string('[wrong]')}", "format string", "[wrong]");
+
+        setConfiguration(createConfigurationBuilder()
+                .dateFormat("[wrong d]")
+                .dateTimeFormat("[wrong dt]")
+                .timeFormat("[wrong t]")
+                .build());
+        assertErrorContains("${.now?date}", "\"date_format\"", "[wrong d]");
+        assertErrorContains("${.now?datetime}", "\"datetime_format\"", "[wrong dt]");
+        assertErrorContains("${.now?time}", "\"time_format\"", "[wrong t]");
+    }
+
+    @Test
+    public void testCustomParameterized() throws Exception {
+        Configuration cfg = getConfiguration();
+        addToDataModel("d", new SimpleDate(new Date(12345678L), TemplateDateModel.DATETIME));
+        setConfigurationWithDateTimeFormat("@div 1000");
+        assertOutput("${d}", "12345");
+        assertOutput("${d?string}", "12345");
+        assertOutput("${d?string.@div_100}", "123456");
+        
+        assertErrorContains("${d?string.@div_xyz}", "\"@div_xyz\"", "\"xyz\"");
+        setConfigurationWithDateTimeFormat("@div");
+        assertErrorContains("${d}", "\"datetime_format\"", "\"@div\"", "format parameter is required");
+    }
+    
+    @Test
+    public void testUnknownCustomFormat() throws Exception {
+        {
+            setConfigurationWithDateTimeFormat("@noSuchFormat");
+            Throwable exc = assertErrorContains(
+                    "${.now}",
+                    "\"@noSuchFormat\"", "\"noSuchFormat\"", "\"datetime_format\"");
+            assertThat(exc.getCause(), instanceOf(UndefinedCustomFormatException.class));
+            
+        }
+        {
+            setConfiguration(createConfigurationBuilder().dateFormat("@noSuchFormatD").build());
+            assertErrorContains(
+                    "${.now?date}",
+                    "\"@noSuchFormatD\"", "\"noSuchFormatD\"", "\"date_format\"");
+        }
+        {
+            setConfiguration(createConfigurationBuilder().timeFormat("@noSuchFormatT").build());
+            assertErrorContains(
+                    "${.now?time}",
+                    "\"@noSuchFormatT\"", "\"noSuchFormatT\"", "\"time_format\"");
+        }
+
+        {
+            setConfigurationWithDateTimeFormat("");
+            Throwable exc = assertErrorContains("${.now?string('@noSuchFormat2')}",
+                    "\"@noSuchFormat2\"", "\"noSuchFormat2\"");
+            assertThat(exc.getCause(), instanceOf(UndefinedCustomFormatException.class));
+        }
+    }
+
+    private void setConfigurationWithDateTimeFormat(String formatString) {
+        setConfiguration(createConfigurationBuilder().dateTimeFormat(formatString).build());
+    }
+
+    @Test
+    public void testNullInModel() throws Exception {
+        addToDataModel("d", new MutableTemplateDateModel());
+        assertErrorContains("${d}", "nothing inside it");
+        assertErrorContains("${d?string}", "nothing inside it");
+    }
+    
+    @Test
+    public void testIcIAndEscaping() throws Exception {
+        addToDataModel("d", new SimpleDate(new Date(12345678L), TemplateDateModel.DATETIME));
+        
+        setConfigurationWithDateTimeFormat("@epoch");
+        assertOutput("${d}", "12345678");
+        setConfigurationWithDateTimeFormat("'@'yyyy");
+        assertOutput("${d}", "@1970");
+        setConfigurationWithDateTimeFormat("@@yyyy");
+        assertOutput("${d}", "@@1970");
+
+        setConfiguration(createConfigurationBuilder()
+                .customDateFormats(Collections.<String, TemplateDateFormatFactory>emptyMap())
+                .dateTimeFormat("@epoch")
+                .build());
+        assertErrorContains("${d}", "custom", "\"epoch\"");
+    }
+
+    @Test
+    public void testEnvironmentGetters() throws Exception {
+        String dateFormatStr = "yyyy.MM.dd. (Z)";
+        String timeFormatStr = "HH:mm";
+        String dateTimeFormatStr = "yyyy.MM.dd. HH:mm";
+
+        setConfiguration(createConfigurationBuilder()
+                .dateFormat(dateFormatStr)
+                .timeFormat(timeFormatStr)
+                .dateTimeFormat(dateTimeFormatStr)
+                .build());
+
+        Configuration cfg = getConfiguration();
+
+        Template t = new Template(null, "", cfg);
+        Environment env = t.createProcessingEnvironment(null, null);
+        
+        // Test that values are coming from the cache if possible
+        for (Class dateClass : new Class[] { Date.class, Timestamp.class, java.sql.Date.class, Time.class } ) {
+            for (int dateType
+                    : new int[] { TemplateDateModel.DATE, TemplateDateModel.TIME, TemplateDateModel.DATETIME }) {
+                String formatString =
+                        dateType == TemplateDateModel.DATE ? cfg.getDateFormat() :
+                        (dateType == TemplateDateModel.TIME ? cfg.getTimeFormat()
+                        : cfg.getDateTimeFormat());
+                TemplateDateFormat expectedF = env.getTemplateDateFormat(formatString, dateType, dateClass);
+                assertSame(expectedF, env.getTemplateDateFormat(dateType, dateClass)); // Note: Only reads the cache
+                assertSame(expectedF, env.getTemplateDateFormat(formatString, dateType, dateClass));
+                assertSame(expectedF, env.getTemplateDateFormat(formatString, dateType, dateClass, cfg.getLocale()));
+                assertSame(expectedF, env.getTemplateDateFormat(formatString, dateType, dateClass, cfg.getLocale(),
+                        cfg.getTimeZone(), cfg.getSQLDateAndTimeTimeZone()));
+            }
+        }
+
+        String dateFormatStr2 = dateFormatStr + "'!'";
+        String timeFormatStr2 = timeFormatStr + "'!'";
+        String dateTimeFormatStr2 = dateTimeFormatStr + "'!'";
+        
+        assertEquals("2015.09.06. 13:00",
+                env.getTemplateDateFormat(TemplateDateModel.DATETIME, Date.class).formatToPlainText(TM));
+        assertEquals("2015.09.06. 13:00!",
+                env.getTemplateDateFormat(dateTimeFormatStr2, TemplateDateModel.DATETIME, Date.class).formatToPlainText(TM));
+        
+        assertEquals("2015.09.06. (+0100)",
+                env.getTemplateDateFormat(TemplateDateModel.DATE, Date.class).formatToPlainText(TM));
+        assertEquals("2015.09.06. (+0100)!",
+                env.getTemplateDateFormat(dateFormatStr2, TemplateDateModel.DATE, Date.class).formatToPlainText(TM));
+        
+        assertEquals("13:00",
+                env.getTemplateDateFormat(TemplateDateModel.TIME, Date.class).formatToPlainText(TM));
+        assertEquals("13:00!",
+                env.getTemplateDateFormat(timeFormatStr2, TemplateDateModel.TIME, Date.class).formatToPlainText(TM));
+        
+        assertEquals("2015.09.06. 13:00",
+                env.getTemplateDateFormat(TemplateDateModel.DATETIME, Timestamp.class).formatToPlainText(TM));
+        assertEquals("2015.09.06. 13:00!",
+                env.getTemplateDateFormat(dateTimeFormatStr2, TemplateDateModel.DATETIME, Timestamp.class).formatToPlainText(TM));
+
+        assertEquals("2015.09.06. (+0000)",
+                env.getTemplateDateFormat(TemplateDateModel.DATE, java.sql.Date.class).formatToPlainText(TM));
+        assertEquals("2015.09.06. (+0000)!",
+                env.getTemplateDateFormat(dateFormatStr2, TemplateDateModel.DATE, java.sql.Date.class).formatToPlainText(TM));
+
+        assertEquals("12:00",
+                env.getTemplateDateFormat(TemplateDateModel.TIME, Time.class).formatToPlainText(TM));
+        assertEquals("12:00!",
+                env.getTemplateDateFormat(timeFormatStr2, TemplateDateModel.TIME, Time.class).formatToPlainText(TM));
+
+        {
+            String dateTimeFormatStrLoc = dateTimeFormatStr + " EEEE";
+            // Gets into cache:
+            TemplateDateFormat format1
+                    = env.getTemplateDateFormat(dateTimeFormatStrLoc, TemplateDateModel.DATETIME, Date.class);
+            assertEquals("2015.09.06. 13:00 Sunday", format1.formatToPlainText(TM));
+            // Different locale (not cached):
+            assertEquals("2015.09.06. 13:00 Sonntag",
+                    env.getTemplateDateFormat(dateTimeFormatStrLoc, TemplateDateModel.DATETIME, Date.class,
+                            Locale.GERMANY).formatToPlainText(TM));
+            // Different locale and zone (not cached):
+            assertEquals("2015.09.06. 14:00 Sonntag",
+                    env.getTemplateDateFormat(dateTimeFormatStrLoc, TemplateDateModel.DATETIME, Date.class,
+                            Locale.GERMANY, TimeZone.getTimeZone("GMT+02"), TimeZone.getTimeZone("GMT+03")).formatToPlainText(TM));
+            // Different locale and zone (not cached):
+            assertEquals("2015.09.06. 15:00 Sonntag",
+                    env.getTemplateDateFormat(dateTimeFormatStrLoc, TemplateDateModel.DATETIME, java.sql.Date.class,
+                            Locale.GERMANY, TimeZone.getTimeZone("GMT+02"), TimeZone.getTimeZone("GMT+03")).formatToPlainText(TM));
+            // Check for corrupted cache:
+            TemplateDateFormat format2
+                    = env.getTemplateDateFormat(dateTimeFormatStrLoc, TemplateDateModel.DATETIME, Date.class);
+            assertEquals("2015.09.06. 13:00 Sunday", format2.formatToPlainText(TM));
+            assertSame(format1, format2);
+        }
+    }
+
+    @Test
+    public void testAliases() throws Exception {
+        setConfiguration(createConfigurationBuilder()
+                .customDateFormats(ImmutableMap.of(
+                        "d", new AliasTemplateDateFormatFactory("yyyy-MMM-dd"),
+                        "m", new AliasTemplateDateFormatFactory("yyyy-MMM"),
+                        "epoch", EpochMillisTemplateDateFormatFactory.INSTANCE))
+                .templateConfigurations(
+                        new ConditionalTemplateConfigurationFactory(
+                                new FileNameGlobMatcher("*2*"),
+                                new TemplateConfiguration.Builder()
+                                        .customDateFormats(ImmutableMap.<String, TemplateDateFormatFactory>of(
+                                                "m", new AliasTemplateDateFormatFactory("yyyy-MMMM"),
+                                                "i", new AliasTemplateDateFormatFactory("@epoch")))
+                                        .build()))
+                .build());
+
+        addToDataModel("d", TM);
+        String commonFtl = "${d?string.@d} ${d?string.@m} "
+                + "<#setting locale='fr_FR'>${d?string.@m} "
+                + "<#attempt>${d?string.@i}<#recover>E</#attempt>";
+        addTemplate("t1.ftl", commonFtl);
+        addTemplate("t2.ftl", commonFtl);
+        
+        // 2015-09-06T12:00:00Z
+        assertOutputForNamed("t1.ftl", "2015-Sep-06 2015-Sep 2015-sept. E");
+        assertOutputForNamed("t2.ftl", "2015-Sep-06 2015-September 2015-septembre " + T);
+    }
+    
+    @Test
+    public void testAliases2() throws Exception {
+        setConfiguration(
+                createConfigurationBuilder()
+                .customDateFormats(ImmutableMap.<String, TemplateDateFormatFactory>of(
+                        "d", new AliasTemplateDateFormatFactory("yyyy-MMM",
+                                ImmutableMap.of(
+                                        new Locale("en"), "yyyy-MMM'_en'",
+                                        Locale.UK, "yyyy-MMM'_en_GB'",
+                                        Locale.FRANCE, "yyyy-MMM'_fr_FR'"))))
+                .dateTimeFormat("@d")
+                .build());
+        addToDataModel("d", TM);
+        assertOutput(
+                "<#setting locale='en_US'>${d} "
+                + "<#setting locale='en_GB'>${d} "
+                + "<#setting locale='en_GB_Win'>${d} "
+                + "<#setting locale='fr_FR'>${d} "
+                + "<#setting locale='hu_HU'>${d}",
+                "2015-Sep_en 2015-Sep_en_GB 2015-Sep_en_GB 2015-sept._fr_FR 2015-szept.");
+    }
+    
+    /**
+     * ?date() and such are new in 2.3.24.
+     */
+    @Test
+    public void testZeroArgDateBI() throws IOException, TemplateException {
+        setConfiguration(
+                createConfigurationBuilder()
+                .dateFormat("@epoch")
+                .dateTimeFormat("@epoch")
+                .timeFormat("@epoch")
+                .build());
+
+        addToDataModel("t", String.valueOf(T));
+        
+        assertOutput(
+                "${t?date?string.xs_u} ${t?date()?string.xs_u}",
+                "2015-09-06Z 2015-09-06Z");
+        assertOutput(
+                "${t?time?string.xs_u} ${t?time()?string.xs_u}",
+                "12:00:00Z 12:00:00Z");
+        assertOutput(
+                "${t?datetime?string.xs_u} ${t?datetime()?string.xs_u}",
+                "2015-09-06T12:00:00Z 2015-09-06T12:00:00Z");
+    }
+
+    @Test
+    public void testAppMetaRoundtrip() throws IOException, TemplateException {
+        setConfiguration(
+                createConfigurationBuilder()
+                .dateFormat("@appMeta")
+                .dateTimeFormat("@appMeta")
+                .timeFormat("@appMeta")
+                .build());
+
+        addToDataModel("t", String.valueOf(T) + "/foo");
+        
+        assertOutput(
+                "${t?date} ${t?date()}",
+                T + " " + T + "/foo");
+        assertOutput(
+                "${t?time} ${t?time()}",
+                T + " " + T + "/foo");
+        assertOutput(
+                "${t?datetime} ${t?datetime()}",
+                T + " " + T + "/foo");
+    }
+    
+    @Test
+    public void testUnknownDateType() throws IOException, TemplateException {
+        addToDataModel("u", new Date(T));
+        assertErrorContains("${u?string}", "isn't known");
+        assertOutput("${u?string('yyyy')}", "2015");
+        assertOutput("<#assign s = u?string>${s('yyyy')}", "2015");
+    }
+    
+    private static class MutableTemplateDateModel implements TemplateDateModel {
+        
+        private Date date;
+
+        public void setDate(Date date) {
+            this.date = date;
+        }
+
+        @Override
+        public Date getAsDate() throws TemplateModelException {
+            return date;
+        }
+
+        @Override
+        public int getDateType() {
+            return DATETIME;
+        }
+        
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/DirectiveCallPlaceTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/DirectiveCallPlaceTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/DirectiveCallPlaceTest.java
new file mode 100644
index 0000000..29220c4
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/DirectiveCallPlaceTest.java
@@ -0,0 +1,249 @@
+/*
+ * 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 java.io.StringWriter;
+import java.io.Writer;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+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.model.TemplateModelException;
+import org.apache.freemarker.core.model.TemplateScalarModel;
+import org.apache.freemarker.core.util.ObjectFactory;
+import org.apache.freemarker.test.TemplateTest;
+import org.junit.Test;
+
+public class DirectiveCallPlaceTest extends TemplateTest {
+
+    @Test
+    public void testCustomDataBasics() throws IOException, TemplateException {
+        addTemplate(
+                "customDataBasics.ftl",
+                "<@u...@uc> <@u...@uc> <@uc>Ab<#-- -->c</...@uc> <@l...@lc> <@l...@lc>");
+        
+        CachingTextConverterDirective.resetCacheRecreationCount();
+        for (int i = 0; i < 3; i++) {
+            assertOutputForNamed(
+                    "customDataBasics.ftl",
+                    "ABC[cached 1] X=123 ABC[cached 2]  abc[cached 3]");
+        }
+    }
+
+    @Test
+    public void testCustomDataProviderMismatch() throws IOException, TemplateException {
+        addTemplate(
+                "customDataProviderMismatch.ftl",
+                "<#list [uc, lc, uc] as d><#list 1..2 as _><@d...@d></#list></#list>");
+        
+        CachingTextConverterDirective.resetCacheRecreationCount();
+        assertOutputForNamed(
+                "customDataProviderMismatch.ftl",
+                "ABC[cached 1]ABC[cached 1]abc[cached 2]abc[cached 2]ABC[cached 3]ABC[cached 3]");
+        assertOutputForNamed(
+                "customDataProviderMismatch.ftl",
+                "ABC[cached 3]ABC[cached 3]abc[cached 4]abc[cached 4]ABC[cached 5]ABC[cached 5]");
+    }
+    
+    @Test
+    public void testPositions() throws IOException, TemplateException {
+        addTemplate(
+                "positions.ftl",
+                "<@pa />\n"
+                + "..<@pa\n"
+                + "/><@pa>xxx</@>\n"
+                + "<@pa>{<@pa/> <@pa/>}</@>\n"
+                + "${curDirLine}<@argP p=curDirLine?string>${curDirLine}</...@argP>${curDirLine}\n"
+                + "<#macro m p>(p=${p}){<#nested>}</#macro>\n"
+                + "${curDirLine}<@m p=curDirLine?string>${curDirLine}</...@m>${curDirLine}");
+        
+        assertOutputForNamed(
+                "positions.ftl",
+                "[positions.ftl:1:1-1:7]"
+                + "..[positions.ftl:2:3-3:2]"
+                + "[positions.ftl:3:3-3:14]xxx\n"
+                + "[positions.ftl:4:1-4:24]{[positions.ftl:4:7-4:12] [positions.ftl:4:14-4:19]}\n"
+                + "-(p=5){-}-\n"
+                + "-(p=7){-}-"
+                );
+    }
+    
+    @SuppressWarnings("boxing")
+    @Override
+    protected Object createDataModel() {
+        Map<String, Object> dm = new HashMap<>();
+        dm.put("uc", new CachingUpperCaseDirective());
+        dm.put("lc", new CachingLowerCaseDirective());
+        dm.put("pa", new PositionAwareDirective());
+        dm.put("argP", new ArgPrinterDirective());
+        dm.put("curDirLine", new CurDirLineScalar());
+        dm.put("x", 123);
+        return dm;
+    }
+
+    private abstract static class CachingTextConverterDirective implements TemplateDirectiveModel {
+
+        /** Only needed for testing. */
+        private static AtomicInteger cacheRecreationCount = new AtomicInteger();
+        
+        /** Only needed for testing. */
+        static void resetCacheRecreationCount() {
+            cacheRecreationCount.set(0);
+        }
+        
+        @Override
+        public void execute(Environment env, Map params, TemplateModel[] loopVars, final TemplateDirectiveBody body)
+                throws TemplateException, IOException {
+            if (body == null) {
+                return;
+            }
+            
+            final String convertedText;
+
+            final DirectiveCallPlace callPlace = env.getCurrentDirectiveCallPlace();
+            if (callPlace.isNestedOutputCacheable()) {
+                try {
+                    convertedText = (String) callPlace.getOrCreateCustomData(
+                            getTextConversionIdentity(), new ObjectFactory<String>() {
+
+                                @Override
+                                public String createObject() throws TemplateException, IOException {
+                                    return convertBodyText(body)
+                                            + "[cached " + cacheRecreationCount.incrementAndGet() + "]";
+                                }
+
+                            });
+                } catch (CallPlaceCustomDataInitializationException e) {
+                    throw new TemplateModelException("Failed to pre-render nested content", e);
+                }
+            } else {
+                convertedText = convertBodyText(body);
+            }
+
+            env.getOut().write(convertedText);
+        }
+
+        protected abstract Class getTextConversionIdentity();
+
+        private String convertBodyText(TemplateDirectiveBody body) throws TemplateException,
+                IOException {
+            StringWriter sw = new StringWriter();
+            body.render(sw);
+            return convertText(sw.toString());
+        }
+        
+        protected abstract String convertText(String s);
+
+    }
+    
+    private static class CachingUpperCaseDirective extends CachingTextConverterDirective {
+
+        @Override
+        protected String convertText(String s) {
+            return s.toUpperCase();
+        }
+        
+        @Override
+        protected Class getTextConversionIdentity() {
+            return CachingUpperCaseDirective.class;
+        }
+        
+    }
+
+    private static class CachingLowerCaseDirective extends CachingTextConverterDirective {
+
+        @Override
+        protected String convertText(String s) {
+            return s.toLowerCase();
+        }
+
+        @Override
+        protected Class getTextConversionIdentity() {
+            return CachingLowerCaseDirective.class;
+        }
+        
+    }
+    
+    private static class PositionAwareDirective implements TemplateDirectiveModel {
+
+        @Override
+        public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body)
+                throws TemplateException, IOException {
+            Writer out = env.getOut();
+            DirectiveCallPlace callPlace = env.getCurrentDirectiveCallPlace();
+            out.write("[");
+            out.write(getTemplateSourceName(callPlace));
+            out.write(":");
+            out.write(Integer.toString(callPlace.getBeginLine()));
+            out.write(":");
+            out.write(Integer.toString(callPlace.getBeginColumn()));
+            out.write("-");
+            out.write(Integer.toString(callPlace.getEndLine()));
+            out.write(":");
+            out.write(Integer.toString(callPlace.getEndColumn()));
+            out.write("]");
+            if (body != null) {
+                body.render(out);
+            }
+        }
+
+        private String getTemplateSourceName(DirectiveCallPlace callPlace) {
+            return ((ASTDirUserDefined) callPlace).getTemplate().getSourceName();
+        }
+        
+    }
+
+    private static class ArgPrinterDirective implements TemplateDirectiveModel {
+
+        @Override
+        public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body)
+                throws TemplateException, IOException {
+            final Writer out = env.getOut();
+            if (params.size() > 0) {
+                out.write("(p=");
+                out.write(((TemplateScalarModel) params.get("p")).getAsString());
+                out.write(")");
+            }
+            if (body != null) {
+                out.write("{");
+                body.render(out);
+                out.write("}");
+            }
+        }
+        
+    }
+    
+    private static class CurDirLineScalar implements TemplateScalarModel {
+
+        @Override
+        public String getAsString() throws TemplateModelException {
+            DirectiveCallPlace callPlace = Environment.getCurrentEnvironment().getCurrentDirectiveCallPlace();
+            return callPlace != null
+                    ? String.valueOf(Environment.getCurrentEnvironment().getCurrentDirectiveCallPlace().getBeginLine())
+                    : "-";
+        }
+        
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/EncodingOverrideTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/EncodingOverrideTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/EncodingOverrideTest.java
new file mode 100644
index 0000000..b1acead
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/EncodingOverrideTest.java
@@ -0,0 +1,62 @@
+/*
+ * 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.io.StringWriter;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+
+import org.apache.freemarker.core.templateresolver.impl.ClassTemplateLoader;
+import org.junit.Test;
+
+public class EncodingOverrideTest {
+
+    @Test
+    public void testMarchingCharset() throws Exception {
+        Template t = createConfig(StandardCharsets.UTF_8).getTemplate("encodingOverride-UTF-8.ftl");
+        assertEquals(StandardCharsets.UTF_8, t.getActualSourceEncoding());
+        checkTemplateOutput(t);
+    }
+
+    @Test
+    public void testDifferentCharset() throws Exception {
+        Template t = createConfig(StandardCharsets.UTF_8).getTemplate("encodingOverride-ISO-8859-1.ftl");
+        assertEquals(StandardCharsets.ISO_8859_1, t.getActualSourceEncoding());
+        checkTemplateOutput(t);
+    }
+
+    private void checkTemplateOutput(Template t) throws TemplateException, IOException {
+        StringWriter out = new StringWriter(); 
+        t.process(Collections.emptyMap(), out);
+        assertEquals("Béka", out.toString());
+    }
+    
+    private Configuration createConfig(Charset charset) {
+       return new Configuration.Builder(Configuration.VERSION_3_0_0)
+               .templateLoader(new ClassTemplateLoader(EncodingOverrideTest.class, ""))
+               .sourceEncoding(charset)
+               .build();
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/EnvironmentGetTemplateVariantsTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/EnvironmentGetTemplateVariantsTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/EnvironmentGetTemplateVariantsTest.java
new file mode 100644
index 0000000..b79559c
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/EnvironmentGetTemplateVariantsTest.java
@@ -0,0 +1,214 @@
+/*
+ * 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.io.Writer;
+import java.util.Collections;
+import java.util.Map;
+
+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.model.impl.SimpleScalar;
+import org.apache.freemarker.core.templateresolver.impl.StringTemplateLoader;
+import org.apache.freemarker.test.TemplateTest;
+import org.apache.freemarker.test.TestConfigurationBuilder;
+import org.junit.Test;
+
+public class EnvironmentGetTemplateVariantsTest extends TemplateTest {
+
+    private static final StringTemplateLoader TEMPLATES = new StringTemplateLoader();
+    static {
+        TEMPLATES.putTemplate("main",
+                "<@tNames />\n"
+                + "---1---\n"
+                + "[imp: <#import 'imp' as i>${i.impIni}]\n"
+                + "---2---\n"
+                + "<@i.impM>"
+                    + "<@tNames />"
+                + "</@>\n"
+                + "---3---\n"
+                + "[inc: <#include 'inc'>]\n"
+                + "---4---\n"
+                + "<@incM>"
+                    + "<@tNames />"
+                + "</@>\n"
+                + "---5---\n"
+                + "[inc2: <#include 'inc2'>]\n"
+                + "---6---\n"
+                + "<#import 'imp2' as i2>"
+                + "<@i.impM2><@tNames /></@>\n"
+                + "---7---\n"
+                + "<#macro mainM>"
+                    + "[mainM: <@tNames /> {<#nested>} <@tNames />]"
+                + "</#macro>"
+                + "[inc3: <#include 'inc3'>]\n"
+                + "<@mainM><@tNames /> <#include 'inc4'> <@tNames /></@>\n"
+                + "<@tNames />\n"
+                + "---8---\n"
+                + "<#function mainF>"
+                    + "<@tNames />"
+                    + "<#return lastTNamesResult>"
+                + "</#function>"
+                + "mainF: ${mainF()}, impF: ${i.impF()}, incF: ${incF()}\n"
+                );
+        TEMPLATES.putTemplate("inc",
+                "<@tNames />\n"
+                + "<#macro incM>"
+                    + "[incM: <@tNames /> {<#nested>}]"
+                + "</#macro>"
+                + "<#function incF>"
+                    + "<@tNames />"
+                    + "<#return lastTNamesResult>"
+                + "</#function>"
+                + "<@incM><@tNames /></@>\n"
+                + "<#if !included!false>[incInc: <#assign included=true><#include 'inc'>]\n</#if>"
+                );
+        TEMPLATES.putTemplate("imp",
+                "<#assign impIni><@tNames /></#assign>\n"
+                + "<#macro impM>"
+                    + "[impM: <@tNames />\n"
+                        + "{<#nested>}\n"
+                        + "[inc: <#include 'inc'>]\n"
+                        + "<@incM><@tNames /></@>\n"
+                    + "]"
+                + "</#macro>"
+                + "<#macro impM2>"
+                    + "[impM2: <@tNames />\n"
+                    + "{<#nested>}\n"
+                    + "<@i2.imp2M><@tNames /></@>\n"
+                    + "]"
+                + "</#macro>"
+                + "<#function impF>"
+                    + "<@tNames />"
+                    + "<#return lastTNamesResult>"
+                + "</#function>"
+                );
+        TEMPLATES.putTemplate("inc2",
+                "<@tNames />\n"
+                + "<@i.impM><@tNames /></@>\n"
+                );
+        TEMPLATES.putTemplate("imp2",
+                "<#macro imp2M>"
+                    + "[imp2M: <@tNames /> {<#nested>}]"
+                + "</#macro>");
+        TEMPLATES.putTemplate("inc3",
+                "<@tNames />\n"
+                + "<@mainM><@tNames /></@>\n"
+                );
+        TEMPLATES.putTemplate("inc4",
+                "<@tNames />"
+                );
+    }
+    
+    @Test
+    public void test() throws IOException, TemplateException {
+        setConfiguration(createConfiguration(Configuration.VERSION_3_0_0));
+        assertOutputForNamed(
+                "main",
+                "<ct=main mt=main>\n"
+                + "---1---\n"
+                + "[imp: <ct=imp mt=main>]\n"
+                + "---2---\n"
+                + "[impM: <ct=imp mt=main>\n"
+                    + "{<ct=main mt=main>}\n"
+                    + "[inc: <ct=inc mt=main>\n"
+                        + "[incM: <ct=inc mt=main> {<ct=inc mt=main>}]\n"
+                        + "[incInc: <ct=inc mt=main>\n"
+                            + "[incM: <ct=inc mt=main> {<ct=inc mt=main>}]\n"
+                        + "]\n"
+                    + "]\n"
+                    + "[incM: <ct=inc mt=main> {<ct=imp mt=main>}]\n"
+                + "]\n"
+                + "---3---\n"
+                + "[inc: <ct=inc mt=main>\n"
+                    + "[incM: <ct=inc mt=main> {<ct=inc mt=main>}]\n"
+                    + "[incInc: <ct=inc mt=main>\n"
+                        + "[incM: <ct=inc mt=main> {<ct=inc mt=main>}]\n"
+                    + "]\n"
+                + "]\n"
+                + "---4---\n"
+                + "[incM: <ct=inc mt=main> {<ct=main mt=main>}]\n"
+                + "---5---\n"
+                + "[inc2: <ct=inc2 mt=main>\n"
+                    + "[impM: <ct=imp mt=main>\n"
+                        + "{<ct=inc2 mt=main>}\n"
+                        + "[inc: <ct=inc mt=main>\n"
+                            + "[incM: <ct=inc mt=main> {<ct=inc mt=main>}]\n"
+                        + "]\n"
+                        + "[incM: <ct=inc mt=main> {<ct=imp mt=main>}]\n"
+                    + "]\n"
+                + "]\n"
+                + "---6---\n"
+                + "[impM2: <ct=imp mt=main>\n"
+                    + "{<ct=main mt=main>}\n"
+                    + "[imp2M: <ct=imp2 mt=main> {<ct=imp mt=main>}]\n"
+                + "]\n"
+                + "---7---\n"
+                + "[inc3: <ct=inc3 mt=main>\n"
+                    + "[mainM: <ct=main mt=main> {<ct=inc3 mt=main>} <ct=main mt=main>]\n"
+                + "]\n"
+                + "[mainM: "
+                    + "<ct=main mt=main> "
+                    + "{<ct=main mt=main> <ct=inc4 mt=main> <ct=main mt=main>} "
+                    + "<ct=main mt=main>"
+                + "]\n"
+                + "<ct=main mt=main>\n"
+                + "---8---\n"
+                + "mainF: <ct=main mt=main>, impF: <ct=imp mt=main>, incF: <ct=inc mt=main>\n"
+                .replaceAll("<t=\\w+", "<t=main"));
+    }
+
+    @Test
+    public void testNotStarted() throws IOException, TemplateException {
+        Template t = new Template("foo", "", createConfiguration(Configuration.VERSION_3_0_0));
+        final Environment env = t.createProcessingEnvironment(null, null);
+        assertSame(t, env.getMainTemplate());
+        assertSame(t, env.getCurrentTemplate());
+    }
+    
+    private Configuration createConfiguration(Version iciVersion) {
+        return new TestConfigurationBuilder(iciVersion)
+                .templateLoader(TEMPLATES)
+                .whitespaceStripping(false)
+                .build();
+    }
+
+    @Override
+    protected Object createDataModel() {
+        return Collections.singletonMap("tNames", new TemplateDirectiveModel() {
+
+            @Override
+            public void execute(Environment env, Map params, TemplateModel[] loopVars, TemplateDirectiveBody body)
+                    throws TemplateException, IOException {
+                Writer out = env.getOut();
+                final String r = "<ct=" + env.getCurrentTemplate().getLookupName() + " mt="
+                        + env.getMainTemplate().getLookupName() + ">";
+                out.write(r);
+                env.setGlobalVariable("lastTNamesResult", new SimpleScalar(r));
+            }
+            
+        });
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/ExceptionTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/ExceptionTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/ExceptionTest.java
new file mode 100644
index 0000000..19c3b6e
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/ExceptionTest.java
@@ -0,0 +1,115 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.freemarker.core;
+
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.Collections;
+import java.util.Locale;
+
+import org.apache.freemarker.core.templateresolver.impl.StringTemplateLoader;
+import org.apache.freemarker.core.util._NullWriter;
+import org.apache.freemarker.test.TestConfigurationBuilder;
+
+import junit.framework.TestCase;
+public class ExceptionTest extends TestCase {
+    
+    public ExceptionTest(String name) {
+        super(name);
+    }
+
+    public void testParseExceptionSerializable() throws IOException, ClassNotFoundException {
+        try {
+            new Template(null, new StringReader("<@>"), new TestConfigurationBuilder().build());
+            fail();
+        } catch (ParseException e) {
+            ByteArrayOutputStream out = new ByteArrayOutputStream();
+            new ObjectOutputStream(out).writeObject(e);
+            new ObjectInputStream(new ByteArrayInputStream(out.toByteArray())).readObject();
+        }
+    }
+
+    public void testTemplateErrorSerializable() throws IOException, ClassNotFoundException {
+        Template tmp = new Template(null, new StringReader("${noSuchVar}"),
+                new TestConfigurationBuilder().build());
+        try {
+            tmp.process(Collections.EMPTY_MAP, new StringWriter());
+            fail();
+        } catch (TemplateException e) {
+            ByteArrayOutputStream out = new ByteArrayOutputStream();
+            new ObjectOutputStream(out).writeObject(e);
+            new ObjectInputStream(new ByteArrayInputStream(out.toByteArray())).readObject();
+        }
+    }
+    
+    @SuppressWarnings("boxing")
+    public void testTemplateExceptionLocationInformation() throws IOException {
+        StringTemplateLoader tl = new StringTemplateLoader();
+        tl.putTemplate("foo_en.ftl", "\n\nxxx${noSuchVariable}");
+
+        Template t = new TestConfigurationBuilder().templateLoader(tl).build()
+                .getTemplate("foo.ftl", Locale.US);
+        try {
+            t.process(null, _NullWriter.INSTANCE);
+            fail();
+        } catch (TemplateException e) {
+            assertEquals("foo.ftl", t.getLookupName());
+            assertEquals("foo.ftl", e.getTemplateLookupName());
+            assertEquals("foo_en.ftl", e.getTemplateSourceName());
+            assertEquals(3, (int) e.getLineNumber());
+            assertEquals(6, (int) e.getColumnNumber());
+            assertEquals(3, (int) e.getEndLineNumber());
+            assertEquals(19, (int) e.getEndColumnNumber());
+            assertThat(e.getMessage(), containsString("foo_en.ftl"));
+            assertThat(e.getMessage(), containsString("noSuchVariable"));
+        }
+    }
+
+    @SuppressWarnings("cast")
+    public void testParseExceptionLocationInformation() throws IOException {
+        StringTemplateLoader tl = new StringTemplateLoader();
+        tl.putTemplate("foo_en.ftl", "\n\nxxx<#noSuchDirective>");
+
+        try {
+            new TestConfigurationBuilder().templateLoader(tl).build()
+                    .getTemplate("foo.ftl", Locale.US);
+            fail();
+        } catch (ParseException e) {
+            System.out.println(e.getMessage());
+            assertEquals("foo_en.ftl", e.getTemplateSourceName());
+            assertEquals("foo.ftl", e.getTemplateLookupName());
+            assertEquals(3, e.getLineNumber());
+            assertEquals(5, e.getColumnNumber());
+            assertEquals(3, e.getEndLineNumber());
+            assertEquals(20, e.getEndColumnNumber());
+            assertThat(e.getMessage(), containsString("foo_en.ftl"));
+            assertThat(e.getMessage(), containsString("noSuchDirective"));
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/GetSourceTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/GetSourceTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/GetSourceTest.java
new file mode 100644
index 0000000..1462bfc
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/GetSourceTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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 org.apache.freemarker.test.TestConfigurationBuilder;
+import org.junit.Test;
+
+public class GetSourceTest {
+
+    @Test
+    public void testGetSource() throws Exception {
+        {
+            // Note: Default tab size is 8.
+            Template t = new Template(null, "a\n\tb\nc",
+                    new TestConfigurationBuilder().build());
+            // A historical quirk we keep for B.C.: it repaces tabs with spaces.
+            assertEquals("a\n        b\nc", t.getSource(1, 1, 1, 3));
+        }
+        
+        {
+            Template t = new Template(null, "a\n\tb\nc",
+                    new TestConfigurationBuilder().tabSize(4).build());
+            assertEquals("a\n    b\nc", t.getSource(1, 1, 1, 3));
+        }
+        
+        {
+            Template t = new Template(null, "a\n\tb\nc",
+                    new TestConfigurationBuilder().tabSize(1).build());
+            // If tab size is 1, it behaves as it always should have: it keeps the tab.
+            assertEquals("a\n\tb\nc", t.getSource(1, 1, 1, 3));
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/HeaderParsingTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/HeaderParsingTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/HeaderParsingTest.java
new file mode 100644
index 0000000..6f8851d
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/HeaderParsingTest.java
@@ -0,0 +1,60 @@
+/*
+ * 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 HeaderParsingTest extends TemplateTest {
+
+    private final Configuration cfgStripWS = new TestConfigurationBuilder().build();
+    private final Configuration cfgNoStripWS = new TestConfigurationBuilder().whitespaceStripping(false).build();
+
+    @Test
+    public void test() throws IOException, TemplateException {
+        assertOutput("<#ftl>text", "text", "text");
+        assertOutput(" <#ftl> text", " text", " text");
+        assertOutput("\n<#ftl>\ntext", "text", "text");
+        assertOutput("\n \n\n<#ftl> \ntext", "text", "text");
+        assertOutput("\n \n\n<#ftl>\n\ntext", "\ntext", "\ntext");
+    }
+    
+    private void assertOutput(final String ftl, String expectedOutStripped, String expectedOutNonStripped)
+            throws IOException, TemplateException {
+        for (int i = 0; i < 4; i++) {
+            String ftlPermutation = ftl;
+            if ((i & 1) == 1) {
+                ftlPermutation = ftlPermutation.replace("<#ftl>", "<#ftl encoding='utf-8'>");
+            }
+            if ((i & 2) == 2) {
+                ftlPermutation = ftlPermutation.replace('<', '[').replace('>', ']');
+            }
+            
+            setConfiguration(cfgStripWS);
+            assertOutput(ftlPermutation, expectedOutStripped);
+            setConfiguration(cfgNoStripWS);
+            assertOutput(ftlPermutation, expectedOutNonStripped);
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/IncludeAndImportConfigurableLayersTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/IncludeAndImportConfigurableLayersTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/IncludeAndImportConfigurableLayersTest.java
new file mode 100644
index 0000000..738b4de
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/IncludeAndImportConfigurableLayersTest.java
@@ -0,0 +1,354 @@
+/*
+ * 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.StringWriter;
+
+import org.apache.freemarker.core.templateresolver.ConditionalTemplateConfigurationFactory;
+import org.apache.freemarker.core.templateresolver.FileNameGlobMatcher;
+import org.apache.freemarker.core.templateresolver.impl.StringTemplateLoader;
+import org.apache.freemarker.test.TestConfigurationBuilder;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+public class IncludeAndImportConfigurableLayersTest {
+
+    @Test
+    public void test3LayerImportNoClashes() throws Exception {
+        TestConfigurationBuilder cfgB = createConfigurationBuilder()
+                .autoImports(ImmutableMap.of("t1", "t1.ftl"))
+                .templateConfigurations(
+                        new ConditionalTemplateConfigurationFactory(
+                                new FileNameGlobMatcher("main.ftl"),
+                                new TemplateConfiguration.Builder()
+                                        .autoImports(ImmutableMap.of("t2", "t2.ftl"))
+                                        .build()));
+        Configuration cfg = cfgB.build();
+
+        {
+            Template t = cfg.getTemplate("main.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+            env.addAutoImport("t3", "t3.ftl");
+    
+            env.process();
+            assertEquals("In main: t1;t2;t3;", sw.toString());
+        }
+
+        {
+            Template t = cfg.getTemplate("main.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+    
+            env.process();
+            assertEquals("In main: t1;t2;", sw.toString());
+        }
+        
+        {
+            Template t = cfg.getTemplate("main2.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+            env.addAutoImport("t3", "t3.ftl");
+    
+            env.process();
+            assertEquals("In main2: t1;t3;", sw.toString());
+        }
+        
+        cfgB.removeAutoImport("t1");
+        cfg = cfgB.build();
+
+        {
+            Template t = cfg.getTemplate("main.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+            env.addAutoImport("t3", "t3.ftl");
+    
+            env.process();
+            assertEquals("In main: t2;t3;", sw.toString());
+        }
+    }
+    
+    @Test
+    public void test3LayerImportClashes() throws Exception {
+        Configuration cfg = createConfigurationBuilder()
+                .autoImports(ImmutableMap.of(
+                        "t1", "t1.ftl",
+                        "t2", "t2.ftl",
+                        "t3", "t3.ftl"))
+                .templateConfigurations(
+                        new ConditionalTemplateConfigurationFactory(
+                                new FileNameGlobMatcher("main.ftl"),
+                                new TemplateConfiguration.Builder()
+                                        .autoImports(ImmutableMap.of("t2", "t2b.ftl"))
+                                        .build()))
+                .build();
+
+        {
+            Template t = cfg.getTemplate("main.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+            env.addAutoImport("t3", "t3b.ftl");
+    
+            env.process();
+            assertEquals("In main: t1;t2b;t3b;", sw.toString());
+        }
+        
+        {
+            Template t = cfg.getTemplate("main2.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+            env.addAutoImport("t3", "t3b.ftl");
+    
+            env.process();
+            assertEquals("In main2: t1;t2;t3b;", sw.toString());
+        }
+        
+        {
+            Template t = cfg.getTemplate("main.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+    
+            env.process();
+            assertEquals("In main: t1;t3;t2b;", sw.toString());
+        }
+    }
+
+    @Test
+    public void test3LayerIncludesNoClashes() throws Exception {
+        TestConfigurationBuilder cfgB = createConfigurationBuilder()
+                .autoIncludes(ImmutableList.of("t1.ftl"))
+                .templateConfigurations(
+                        new ConditionalTemplateConfigurationFactory(
+                                new FileNameGlobMatcher("main.ftl"),
+                                new TemplateConfiguration.Builder()
+                                        .autoIncludes(ImmutableList.of("t2.ftl"))
+                                        .build()));
+
+        Configuration cfg = cfgB.build();
+
+        {
+            Template t = cfg.getTemplate("main.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+            env.addAutoInclude("t3.ftl");
+    
+            env.process();
+            assertEquals("T1;T2;T3;In main: t1;t2;t3;", sw.toString());
+        }
+
+        {
+            Template t = cfg.getTemplate("main.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+    
+            env.process();
+            assertEquals("T1;T2;In main: t1;t2;", sw.toString());
+        }
+        
+        {
+            Template t = cfg.getTemplate("main2.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+            env.addAutoInclude("t3.ftl");
+    
+            env.process();
+            assertEquals("T1;T3;In main2: t1;t3;", sw.toString());
+        }
+        
+        cfgB.removeAutoInclude("t1.ftl");
+        cfg = cfgB.build();
+
+        {
+            Template t = cfg.getTemplate("main.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+            env.addAutoInclude("t3.ftl");
+    
+            env.process();
+            assertEquals("T2;T3;In main: t2;t3;", sw.toString());
+        }
+    }
+
+    @Test
+    public void test3LayerIncludeClashes() throws Exception {
+        Configuration cfg = createConfigurationBuilder()
+                .autoIncludes(ImmutableList.of(
+                        "t1.ftl",
+                        "t2.ftl",
+                        "t3.ftl"))
+                .templateConfigurations(new ConditionalTemplateConfigurationFactory(
+                        new FileNameGlobMatcher("main.ftl"),
+                        new TemplateConfiguration.Builder()
+                                .autoIncludes(ImmutableList.of("t2.ftl"))
+                                .build()))
+                .build();
+
+        {
+            Template t = cfg.getTemplate("main.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+            env.addAutoInclude("t3.ftl");
+    
+            env.process();
+            assertEquals("T1;T2;T3;In main: t1;t2;t3;", sw.toString());
+        }
+        
+        {
+            Template t = cfg.getTemplate("main2.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+            env.addAutoInclude("t3.ftl");
+    
+            env.process();
+            assertEquals("T1;T2;T3;In main2: t1;t2;t3;", sw.toString());
+        }
+        
+        {
+            Template t = cfg.getTemplate("main.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+    
+            env.process();
+            assertEquals("T1;T3;T2;In main: t1;t3;t2;", sw.toString());
+        }
+        
+        {
+            Template t = cfg.getTemplate("main.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+            env.addAutoInclude("t1.ftl");
+    
+            env.process();
+            assertEquals("T3;T2;T1;In main: t3;t2;t1;", sw.toString());
+        }
+    }
+    
+    @Test
+    public void test3LayerIncludesClashes2() throws Exception {
+        Configuration cfg = createConfigurationBuilder()
+                .autoIncludes(ImmutableList.of("t1.ftl", "t1.ftl"))
+                .templateConfigurations(
+                        new ConditionalTemplateConfigurationFactory(
+                                new FileNameGlobMatcher("main.ftl"),
+                                new TemplateConfiguration.Builder()
+                                        .autoIncludes(ImmutableList.of("t2.ftl", "t2.ftl"))
+                                        .build()))
+                .build();
+
+        {
+            Template t = cfg.getTemplate("main.ftl");
+            StringWriter sw = new StringWriter();
+            Environment env = t.createProcessingEnvironment(null, sw);
+            env.addAutoInclude("t3.ftl");
+            env.addAutoInclude("t3.ftl");
+            env.addAutoInclude("t1.ftl");
+            env.addAutoInclude("t1.ftl");
+    
+            env.process();
+            assertEquals("T2;T3;T1;In main: t2;t3;t1;", sw.toString());
+        }
+    }
+    
+    @Test
+    public void test3LayerLaziness() throws Exception {
+        for (Class<?> layer : new Class<?>[] { Configuration.class, Template.class, Environment.class }) {
+            test3LayerLaziness(layer, null, null, false, "t1;t2;");
+            test3LayerLaziness(layer, null, null, true, "t1;t2;");
+            test3LayerLaziness(layer, null, false, true, "t1;t2;");
+            test3LayerLaziness(layer, null, true, true, "t2;");
+            
+            test3LayerLaziness(layer, false, null, false, "t1;t2;");
+            test3LayerLaziness(layer, false, null, true, "t1;t2;");
+            test3LayerLaziness(layer, false, false, true, "t1;t2;");
+            test3LayerLaziness(layer, false, true, true, "t2;");
+
+            test3LayerLaziness(layer, true, null, false, "");
+            test3LayerLaziness(layer, true, null, true, "");
+            test3LayerLaziness(layer, true, false, true, "t1;");
+            test3LayerLaziness(layer, true, true, true, "");
+        }
+    }
+    
+    private void test3LayerLaziness(
+            Class<?> layer,
+            Boolean lazyImports,
+            Boolean lazyAutoImports, boolean setLazyAutoImports,
+            String expectedOutput)
+            throws Exception {
+        Configuration cfg;
+        {
+            TestConfigurationBuilder cfgB = createConfigurationBuilder()
+                    .autoImports(ImmutableMap.of("t1", "t1.ftl"));
+            if (layer == Configuration.class) {
+                setLazinessOfConfigurable(cfgB, lazyImports, lazyAutoImports, setLazyAutoImports);
+            }
+            cfg = cfgB.build();
+        }
+
+        TemplateConfiguration tc;
+        if (layer == Template.class) {
+            TemplateConfiguration.Builder tcb = new TemplateConfiguration.Builder();
+            setLazinessOfConfigurable(tcb, lazyImports, lazyAutoImports, setLazyAutoImports);
+            tc = tcb.build();
+        } else {
+            tc = null;
+        }
+
+        Template t = new Template(null, "<#import 't2.ftl' as t2>${loaded!}", cfg, tc);
+        StringWriter sw = new StringWriter();
+
+        Environment env = t.createProcessingEnvironment(null, sw);
+        if (layer == Environment.class) {
+            setLazinessOfConfigurable(env, lazyImports, lazyAutoImports, setLazyAutoImports);
+        }
+
+        env.process();
+        assertEquals(expectedOutput, sw.toString());
+    }
+
+    private void setLazinessOfConfigurable(
+            MutableProcessingConfiguration<?> cfg,
+            Boolean lazyImports, Boolean lazyAutoImports, boolean setLazyAutoImports) {
+        if (lazyImports != null) {
+            cfg.setLazyImports(lazyImports);
+        }
+        if (setLazyAutoImports) {
+            cfg.setLazyAutoImports(lazyAutoImports);
+        }
+    }
+    
+    private TestConfigurationBuilder createConfigurationBuilder() {
+        StringTemplateLoader loader = new StringTemplateLoader();
+        loader.putTemplate("main.ftl", "In main: ${loaded}");
+        loader.putTemplate("main2.ftl", "In main2: ${loaded}");
+        loader.putTemplate("t1.ftl", "<#global loaded = (loaded!) + 't1;'>T1;");
+        loader.putTemplate("t2.ftl", "<#global loaded = (loaded!) + 't2;'>T2;");
+        loader.putTemplate("t3.ftl", "<#global loaded = (loaded!) + 't3;'>T3;");
+        loader.putTemplate("t1b.ftl", "<#global loaded = (loaded!) + 't1b;'>T1b;");
+        loader.putTemplate("t2b.ftl", "<#global loaded = (loaded!) + 't2b;'>T2b;");
+        loader.putTemplate("t3b.ftl", "<#global loaded = (loaded!) + 't3b;'>T3b;");
+
+        return new TestConfigurationBuilder().templateLoader(loader);
+    }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/IncludeAndImportTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/IncludeAndImportTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/IncludeAndImportTest.java
new file mode 100644
index 0000000..fa767ff
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/IncludeAndImportTest.java
@@ -0,0 +1,270 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.freemarker.core;
+
+import static org.hamcrest.Matchers.*;
+import static org.junit.Assert.*;
+
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+import org.apache.freemarker.core.Environment.LazilyInitializedNamespace;
+import org.apache.freemarker.core.Environment.Namespace;
+import org.apache.freemarker.core.model.WrappingTemplateModel;
+import org.apache.freemarker.test.TemplateTest;
+import org.apache.freemarker.test.TestConfigurationBuilder;
+import org.junit.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+@SuppressWarnings("boxing")
+public class IncludeAndImportTest extends TemplateTest {
+
+    @Override
+    protected void addCommonTemplates() {
+        addTemplate("inc1.ftl", "[inc1]<#global inc1Cnt = (inc1Cnt!0) + 1><#global history = (history!) + 'I'>");
+        addTemplate("inc2.ftl", "[inc2]");
+        addTemplate("inc3.ftl", "[inc3]");
+        addTemplate("lib1.ftl", "<#global lib1Cnt = (lib1Cnt!0) + 1><#global history = (history!) + 'L1'>"
+                + "<#macro m>In lib1</#macro>");
+        addTemplate("lib2.ftl", "<#global history = (history!) + 'L2'>"
+                + "<#macro m>In lib2</#macro>");
+        addTemplate("lib3.ftl", "<#global history = (history!) + 'L3'>"
+                + "<#macro m>In lib3</#macro>");
+        
+        addTemplate("lib2CallsLib1.ftl", "<#global history = (history!) + 'L2'>"
+                + "<#macro m>In lib2 (<@lib1.m/>)</#macro>");
+        addTemplate("lib3ImportsLib1.ftl", "<#import 'lib1.ftl' as lib1><#global history = (history!) + 'L3'>"
+                + "<#macro m>In lib3 (<@lib1.m/>)</#macro>");
+        
+        addTemplate("lib_de.ftl", "<#global history = (history!) + 'LDe'><#assign initLocale=.locale>"
+                + "<#macro m>de</#macro>");
+        addTemplate("lib_en.ftl", "<#global history = (history!) + 'LEn'><#assign initLocale=.locale>"
+                + "<#macro m>en</#macro>");
+    }
+
+    @Test
+    public void includeSameTwice() throws IOException, TemplateException {
+        assertOutput("<#include 'inc1.ftl'>${inc1Cnt}<#include 'inc1.ftl'>${inc1Cnt}", "[inc1]1[inc1]2");
+    }
+
+    @Test
+    public void importSameTwice() throws IOException, TemplateException {
+        assertOutput("<#import 'lib1.ftl' as i1>${lib1Cnt} <#import 'lib1.ftl' as i2>${lib1Cnt}", "1 1");
+    }
+
+    @Test
+    public void importInMainCreatesGlobal() throws IOException, TemplateException {
+        String ftl = "${.main.lib1???c} ${.globals.lib1???c}"
+                + "<#import 'lib1.ftl' as lib1> ${.main.lib1???c} ${.globals.lib1???c}";
+        String expectedOut = "false false true true";
+        assertOutput(ftl, expectedOut);
+    }
+    
+    @Test
+    public void importInMainCreatesGlobalBugfix() throws IOException, TemplateException {
+        // An import in the main namespace should invoke a global variable, even if the imported library was already
+        // initialized elsewhere.
+        String ftl = "<#import 'lib3ImportsLib1.ftl' as lib3>${lib1Cnt} ${.main.lib1???c} ${.globals.lib1???c}, "
+        + "<#import 'lib1.ftl' as lib1>${lib1Cnt} ${.main.lib1???c} ${.globals.lib1???c}";
+        assertOutput(ftl, "1 false false, 1 true true");
+    }
+
+    /**
+     * Tests the order of auto-includes and auto-imports, also that they only effect the main template directly.
+     */
+    @Test
+    public void autoIncludeAndAutoImport() throws IOException, TemplateException {
+        setConfiguration(new TestConfigurationBuilder()
+                .autoImports(ImmutableMap.of(
+                        "lib1", "lib1.ftl",
+                        "lib2", "lib2CallsLib1.ftl"
+                ))
+                .autoIncludes(ImmutableList.of(
+                        "inc1.ftl",
+                        "inc2.ftl"))
+                .build());
+        assertOutput(
+                "<#include 'inc3.ftl'>[main] ${inc1Cnt}, ${history}, <@lib1.m/>, <@lib2.m/>",
+                "[inc1][inc2][inc3][main] 1, L1L2I, In lib1, In lib2 (In lib1)");
+    }
+    
+    /**
+     * Demonstrates design issue in FreeMarker 2.3.x where the lookupStrategy is not factored in when identifying
+     * already existing namespaces.
+     */
+    @Test
+    public void lookupSrategiesAreNotConsideredProperly() throws IOException, TemplateException {
+        // As only the name of the template is used for the finding the already existing namespace, the settings that
+        // influence the lookup are erroneously ignored.
+        assertOutput(
+                "<#setting locale='en_US'><#import 'lib.ftl' as ns1>"
+                + "<#setting locale='de_DE'><#import 'lib.ftl' as ns2>"
+                + "<@ns1.m/> <@ns2.m/> ${history}",
+                "en en LEn");
+        
+        // The opposite of the prevous, where differn names refer to the same template after a lookup: 
+        assertOutput(
+                "<#setting locale='en_US'>"
+                + "<#import '*/lib.ftl' as ns1>"
+                + "<#import 'lib.ftl' as ns2>"
+                + "<@ns1.m/> <@ns2.m/> ${history}",
+                "en en LEnLEn");
+    }
+    
+    @Test
+    public void lazyImportBasics() throws IOException, TemplateException {
+        String ftlImports = "<#import 'lib1.ftl' as l1><#import 'lib2.ftl' as l2><#import 'lib3ImportsLib1.ftl' as l3>";
+        String ftlCalls = "<@l2.m/>, <@l1.m/>; ${history}";
+        String ftl = ftlImports + ftlCalls;
+        
+        assertOutput(ftl, "In lib2, In lib1; L1L2L3");
+        
+        setConfiguration(new TestConfigurationBuilder().lazyImports(true).build());
+        assertOutput(ftl, "In lib2, In lib1; L2L1");
+        
+        assertOutput(ftlImports + "<@l3.m/>, " + ftlCalls, "In lib3 (In lib1), In lib2, In lib1; L3L1L2");
+    }
+
+    @Test
+    public void lazyImportAndLocale() throws IOException, TemplateException {
+        setConfiguration(new TestConfigurationBuilder().lazyImports(true).build());
+        assertOutput("<#setting locale = 'de_DE'><#import 'lib.ftl' as lib>"
+                + "[${history!}] "
+                + "<#setting locale = 'en'>"
+                + "<@lib.m/> ${lib.initLocale} [${history}]",
+                "[] de de_DE [LDe]");
+    }
+
+    @Test
+    public void lazyAutoImportSettings() throws IOException, TemplateException {
+        TestConfigurationBuilder cfgB = new TestConfigurationBuilder()
+                .autoImports(ImmutableMap.of(
+                        "l1", "lib1.ftl",
+                        "l2", "lib2.ftl",
+                        "l3", "lib3.ftl"
+                ));
+
+        String ftl = "<@l2.m/>, <@l1.m/>; ${history}";
+        String expectedEagerOutput = "In lib2, In lib1; L1L2L3";
+        String expecedLazyOutput = "In lib2, In lib1; L2L1";
+
+        setConfiguration(cfgB.build());
+        assertOutput(ftl, expectedEagerOutput);
+        cfgB.setLazyImports(true);
+        setConfiguration(cfgB.build());
+        assertOutput(ftl, expecedLazyOutput);
+        cfgB.setLazyImports(false);
+        setConfiguration(cfgB.build());
+        assertOutput(ftl, expectedEagerOutput);
+        cfgB.setLazyAutoImports(true);
+        setConfiguration(cfgB.build());
+        assertOutput(ftl, expecedLazyOutput);
+        cfgB.setLazyAutoImports(null);
+        setConfiguration(cfgB.build());
+        assertOutput(ftl, expectedEagerOutput);
+        cfgB.setLazyImports(true);
+        cfgB.setLazyAutoImports(false);
+        setConfiguration(cfgB.build());
+        assertOutput(ftl, expectedEagerOutput);
+    }
+    
+    @Test
+    public void lazyAutoImportMixedWithManualImport() throws IOException, TemplateException {
+        TestConfigurationBuilder cfgB = new TestConfigurationBuilder()
+                .autoImports(ImmutableMap.of(
+                        "l1", "lib1.ftl",
+                        "l2", "/./lib2.ftl",
+                        "l3", "lib3.ftl"))
+                .lazyAutoImports(true);
+
+        String ftl = "<@l2.m/>, <@l1.m/>; ${history}";
+        String expectOutputWithoutHistory = "In lib2, In lib1; ";
+        String expecedOutput = expectOutputWithoutHistory + "L2L1";
+
+        setConfiguration(cfgB.build());
+        assertOutput(ftl, expecedOutput);
+        assertOutput("<#import 'lib1.ftl' as l1>" + ftl, expectOutputWithoutHistory + "L1L2");
+        assertOutput("<#import './x/../lib1.ftl' as l1>" + ftl, expectOutputWithoutHistory + "L1L2");
+        assertOutput("<#import 'lib2.ftl' as l2>" + ftl, expecedOutput);
+        assertOutput("<#import 'lib3.ftl' as l3>" + ftl, expectOutputWithoutHistory + "L3L2L1");
+
+        cfgB.setLazyImports(true);
+        setConfiguration(cfgB.build());
+        assertOutput("<#import 'lib1.ftl' as l1>" + ftl, expecedOutput);
+        assertOutput("<#import './x/../lib1.ftl' as l1>" + ftl, expecedOutput);
+        assertOutput("<#import 'lib2.ftl' as l2>" + ftl, expecedOutput);
+        assertOutput("<#import 'lib3.ftl' as l3>" + ftl, expecedOutput);
+    }
+
+    @Test
+    public void lazyImportErrors() throws IOException, TemplateException {
+        TestConfigurationBuilder cfgB = new TestConfigurationBuilder();
+        cfgB.setLazyImports(true);
+
+        setConfiguration(cfgB.build());
+        assertOutput("<#import 'noSuchTemplate.ftl' as wrong>x", "x");
+        
+        cfgB.addAutoImport("wrong", "noSuchTemplate.ftl");
+        setConfiguration(cfgB.build());
+        assertOutput("x", "x");
+
+        try {
+            assertOutput("${wrong.x}", "");
+            fail();
+        } catch (TemplateException e) {
+            assertThat(e.getMessage(),
+                    allOf(containsString("Lazy initialization"), containsString("noSuchTemplate.ftl")));
+            assertThat(e.getCause(), instanceOf(TemplateNotFoundException.class));
+        }
+        
+        addTemplate("containsError.ftl", "${noSuchVar}");
+        try {
+            assertOutput("<#import 'containsError.ftl' as lib>${lib.x}", "");
+            fail();
+        } catch (TemplateException e) {
+            assertThat(e.getMessage(),
+                    allOf(containsString("Lazy initialization"), containsString("containsError.ftl")));
+            assertThat(e.getCause(), instanceOf(InvalidReferenceException.class));
+            assertThat(e.getCause().getMessage(), containsString("noSuchVar"));
+        }
+    }
+    
+    /**
+     * Ensures that all methods are overridden so that they will do the lazy initialization.
+     */
+    @Test
+    public void lazilyInitializingNamespaceOverridesAll() throws SecurityException, NoSuchMethodException {
+        for (Method m : Namespace.class.getMethods()) {
+            Class<?> declClass = m.getDeclaringClass();
+            if (declClass == Object.class || declClass == WrappingTemplateModel.class
+                    || (m.getModifiers() & Modifier.STATIC) != 0
+                    || m.getName().equals("synchronizedWrapper")) {
+                continue;
+            }
+            Method lazyM = LazilyInitializedNamespace.class.getMethod(m.getName(), m.getParameterTypes());
+            if (lazyM.getDeclaringClass() != LazilyInitializedNamespace.class) {
+                fail("The " + lazyM + " method wasn't overidden in " + LazilyInitializedNamespace.class.getName());
+            }
+        }
+    }
+    
+}

http://git-wip-us.apache.org/repos/asf/incubator-freemarker/blob/28a276c8/freemarker-core-test/src/test/java/org/apache/freemarker/core/IncudeFromNamelessTest.java
----------------------------------------------------------------------
diff --git a/freemarker-core-test/src/test/java/org/apache/freemarker/core/IncudeFromNamelessTest.java b/freemarker-core-test/src/test/java/org/apache/freemarker/core/IncudeFromNamelessTest.java
new file mode 100644
index 0000000..f4908b8
--- /dev/null
+++ b/freemarker-core-test/src/test/java/org/apache/freemarker/core/IncudeFromNamelessTest.java
@@ -0,0 +1,58 @@
+/*
+ * 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 java.io.StringReader;
+import java.io.StringWriter;
+
+import org.apache.freemarker.core.templateresolver.impl.StringTemplateLoader;
+import org.apache.freemarker.test.TestConfigurationBuilder;
+
+import junit.framework.TestCase;
+
+public class IncudeFromNamelessTest extends TestCase {
+
+    public IncudeFromNamelessTest(String name) {
+        super(name);
+    }
+    
+    public void test() throws IOException, TemplateException {
+        StringTemplateLoader loader = new StringTemplateLoader();
+        loader.putTemplate("i.ftl", "[i]");
+        loader.putTemplate("sub/i.ftl", "[sub/i]");
+        loader.putTemplate("import.ftl", "<#assign x = 1>");
+
+        Configuration cfg = new TestConfigurationBuilder().templateLoader(loader).build();
+
+        Template t = new Template(null, new StringReader(
+                    "<#include 'i.ftl'>\n"
+                    + "<#include '/i.ftl'>\n"
+                    + "<#include 'sub/i.ftl'>\n"
+                    + "<#include '/sub/i.ftl'>"
+                    + "<#import 'import.ftl' as i>${i.x}"
+                ),
+                cfg);
+        StringWriter out = new StringWriter();
+        t.process(null, out);
+        assertEquals("[i][i][sub/i][sub/i]1", out.toString());
+    }
+
+}